diff --git a/UI/volume-control.cpp b/frontend/components/VolumeSlider.cpp similarity index 100% rename from UI/volume-control.cpp rename to frontend/components/VolumeSlider.cpp diff --git a/UI/volume-control.hpp b/frontend/components/VolumeSlider.hpp similarity index 100% rename from UI/volume-control.hpp rename to frontend/components/VolumeSlider.hpp diff --git a/UI/window-basic-main-outputs.cpp b/frontend/utility/AdvancedOutput.cpp similarity index 100% rename from UI/window-basic-main-outputs.cpp rename to frontend/utility/AdvancedOutput.cpp diff --git a/frontend/utility/AdvancedOutput.hpp b/frontend/utility/AdvancedOutput.hpp new file mode 100644 index 000000000..30d203174 --- /dev/null +++ b/frontend/utility/AdvancedOutput.hpp @@ -0,0 +1,2541 @@ +#include +#include +#include +#include +#include +#include +#include "audio-encoders.hpp" +#include "multitrack-video-error.hpp" +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam.hpp" + +using namespace std; + +extern bool EncoderAvailable(const char *encoder); + +volatile bool streaming_active = false; +volatile bool recording_active = false; +volatile bool recording_paused = false; +volatile bool replaybuf_active = false; +volatile bool virtualcam_active = false; + +#define RTMP_PROTOCOL "rtmp" +#define SRT_PROTOCOL "srt" +#define RIST_PROTOCOL "rist" + +static void OBSStreamStarting(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + return; + + output->delayActive = true; + QMetaObject::invokeMethod(output->main, "StreamDelayStarting", Q_ARG(int, sec)); +} + +static void OBSStreamStopping(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + QMetaObject::invokeMethod(output->main, "StreamStopping"); + else + QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); +} + +static void OBSStartStreaming(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->streamingActive = true; + os_atomic_set_bool(&streaming_active, true); + QMetaObject::invokeMethod(output->main, "StreamingStart"); +} + +static void OBSStopStreaming(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->streamingActive = false; + output->delayActive = false; + output->multitrackVideoActive = false; + os_atomic_set_bool(&streaming_active, false); + QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSStartRecording(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->recordingActive = true; + os_atomic_set_bool(&recording_active, true); + QMetaObject::invokeMethod(output->main, "RecordingStart"); +} + +static void OBSStopRecording(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->recordingActive = false; + os_atomic_set_bool(&recording_active, false); + os_atomic_set_bool(&recording_paused, false); + QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSRecordStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "RecordStopping"); +} + +static void OBSRecordFileChanged(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + const char *next_file = calldata_string(params, "next_file"); + + QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); + + QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); + + output->lastRecordingPath = next_file; +} + +static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->replayBufferActive = true; + os_atomic_set_bool(&replaybuf_active, true); + QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); +} + +static void OBSStopReplayBuffer(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->replayBufferActive = false; + os_atomic_set_bool(&replaybuf_active, false); + QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); +} + +static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); +} + +static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); +} + +static void OBSStartVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->virtualCamActive = true; + os_atomic_set_bool(&virtualcam_active, true); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); +} + +static void OBSStopVirtualCam(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->virtualCamActive = false; + os_atomic_set_bool(&virtualcam_active, false); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); +} + +static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->DestroyVirtualCamView(); +} + +/* ------------------------------------------------------------------------ */ + +struct StartMultitrackVideoStreamingGuard { + StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; + ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } + + std::shared_future GetFuture() const { return future; } + + static std::shared_future MakeReadyFuture() + { + StartMultitrackVideoStreamingGuard guard; + return guard.GetFuture(); + } + +private: + std::promise guard; + std::shared_future future; +}; + +/* ------------------------------------------------------------------------ */ + +static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +static bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + +static const char *GetStreamOutputType(const obs_service_t *service) +{ + const char *protocol = obs_service_get_protocol(service); + const char *output = nullptr; + + if (!protocol) { + blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); + return nullptr; + } + + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); + return nullptr; + } + + /* Check if the service has a preferred output type */ + output = obs_service_get_preferred_output_type(service); + if (output) { + if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) + return output; + + blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); + } + + /* Otherwise, prefer first-party output types */ + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + return "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { + return "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { + return "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + obs_enum_output_types_with_protocol(protocol, &output, return_first_id); + if (output) + return output; + + blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); + + return nullptr; +} + +/* ------------------------------------------------------------------------ */ + +inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) +{ + if (main->vcamEnabled) { + virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); + + signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); + startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); + stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); + deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); + } + + auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); + if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { + auto service = main_->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); + } + if (multitrack_enabled) + multitrackVideo = make_unique(); +} + +extern void log_vcam_changed(const VCamConfig &config, bool starting); + +bool BasicOutputHandler::StartVirtualCam() +{ + if (!main->vcamEnabled) + return false; + + bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; + + if (!virtualCamView && !typeIsProgram) + virtualCamView = obs_view_create(); + + UpdateVirtualCamOutputSource(); + + if (!virtualCamVideo) { + virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); + + if (!virtualCamVideo) + return false; + } + + obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); + if (!Active()) + SetupOutputs(); + + bool success = obs_output_start(virtualCam); + if (!success) { + QString errorReason; + + const char *error = obs_output_get_last_error(virtualCam); + if (error) { + errorReason = QT_UTF8(error); + } else { + errorReason = QTStr("Output.StartFailedGeneric"); + } + + QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); + + DestroyVirtualCamView(); + } + + log_vcam_changed(main->vcamConfig, true); + + return success; +} + +void BasicOutputHandler::StopVirtualCam() +{ + if (main->vcamEnabled) { + obs_output_stop(virtualCam); + } +} + +bool BasicOutputHandler::VirtualCamActive() const +{ + if (main->vcamEnabled) { + return obs_output_active(virtualCam); + } + return false; +} + +void BasicOutputHandler::UpdateVirtualCamOutputSource() +{ + if (!main->vcamEnabled || !virtualCamView) + return; + + OBSSourceAutoRelease source; + + switch (main->vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + DestroyVirtualCameraScene(); + return; + case VCamOutputType::PreviewOutput: { + DestroyVirtualCameraScene(); + OBSSource s = main->GetCurrentSceneSource(); + obs_source_get_ref(s); + source = s.Get(); + break; + } + case VCamOutputType::SceneOutput: + DestroyVirtualCameraScene(); + source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); + + if (!vCamSourceScene) + vCamSourceScene = obs_scene_create_private("vcam_source"); + source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); + + if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { + obs_sceneitem_remove(vCamSourceSceneItem); + vCamSourceSceneItem = nullptr; + } + + if (!vCamSourceSceneItem) { + vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); + + obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); + obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); + + const struct vec2 size = { + (float)obs_source_get_width(source), + (float)obs_source_get_height(source), + }; + obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); + } + break; + } + + OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); + if (source != current) + obs_view_set_source(virtualCamView, 0, source); +} + +void BasicOutputHandler::DestroyVirtualCamView() +{ + if (main->vcamConfig.type == VCamOutputType::ProgramView) { + virtualCamVideo = nullptr; + return; + } + + obs_view_remove(virtualCamView); + obs_view_set_source(virtualCamView, 0, nullptr); + virtualCamVideo = nullptr; + + obs_view_destroy(virtualCamView); + virtualCamView = nullptr; + + DestroyVirtualCameraScene(); +} + +void BasicOutputHandler::DestroyVirtualCameraScene() +{ + if (!vCamSourceScene) + return; + + obs_scene_release(vCamSourceScene); + vCamSourceScene = nullptr; + vCamSourceSceneItem = nullptr; +} + +/* ------------------------------------------------------------------------ */ + +struct SimpleOutput : BasicOutputHandler { + OBSEncoder audioStreaming; + OBSEncoder videoStreaming; + OBSEncoder audioRecording; + OBSEncoder audioArchive; + OBSEncoder videoRecording; + OBSEncoder audioTrack[MAX_AUDIO_MIXES]; + + string videoEncoder; + string videoQuality; + bool usingRecordingPreset = false; + bool recordingConfigured = false; + bool ffmpegOutput = false; + bool lowCPUx264 = false; + + SimpleOutput(OBSBasic *main_); + + int CalcCRF(int crf); + + void UpdateRecordingSettings_x264_crf(int crf); + void UpdateRecordingSettings_qsv11(int crf, bool av1); + void UpdateRecordingSettings_nvenc(int cqp); + void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); + void UpdateRecordingSettings_amd_cqp(int cqp); + void UpdateRecordingSettings_apple(int quality); +#ifdef ENABLE_HEVC + void UpdateRecordingSettings_apple_hevc(int quality); +#endif + void UpdateRecordingSettings(); + void UpdateRecordingAudioSettings(); + virtual void Update() override; + + void SetupOutputs() override; + int GetAudioBitrate() const; + + void LoadRecordingPreset_Lossy(const char *encoder); + void LoadRecordingPreset_Lossless(); + void LoadRecordingPreset(); + + void LoadStreamingPreset_Lossy(const char *encoder); + + void UpdateRecording(); + bool ConfigureRecording(bool useReplayBuffer); + + bool IsVodTrackEnabled(obs_service_t *service); + void SetupVodTrack(obs_service_t *service); + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; +}; + +void SimpleOutput::LoadRecordingPreset_Lossless() +{ + fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(simple output)"; + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "format_name", "avi"); + obs_data_set_string(settings, "video_encoder", "utvideo"); + obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); + + obs_output_update(fileOutput, settings); +} + +void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) +{ + videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); + if (!videoRecording) + throw "Failed to create video recording encoder (simple output)"; + obs_encoder_release(videoRecording); +} + +void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) +{ + videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); + if (!videoStreaming) + throw "Failed to create video streaming encoder (simple output)"; + obs_encoder_release(videoStreaming); +} + +/* mistakes have been made to lead us to this. */ +const char *get_simple_output_encoder(const char *encoder) +{ + if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + return "obs_qsv11_v2"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + return "obs_qsv11_av1"; + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + return "h264_texture_amf"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + return "h265_texture_amf"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + return "av1_texture_amf"; + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + return "obs_nvenc_av1_tex"; + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.avc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.hevc"; +#endif + } + + return "obs_x264"; +} + +void SimpleOutput::LoadRecordingPreset() +{ + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); + + videoEncoder = encoder; + videoQuality = quality; + ffmpegOutput = false; + + if (strcmp(quality, "Stream") == 0) { + videoRecording = videoStreaming; + audioRecording = audioStreaming; + usingRecordingPreset = false; + return; + + } else if (strcmp(quality, "Lossless") == 0) { + LoadRecordingPreset_Lossless(); + usingRecordingPreset = true; + ffmpegOutput = true; + return; + + } else { + lowCPUx264 = false; + + if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) + lowCPUx264 = true; + LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); + usingRecordingPreset = true; + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); + else + success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); + + if (!success) + throw "Failed to create audio recording encoder " + "(simple output)"; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[23]; + if (strcmp(audio_encoder, "opus") == 0) { + snprintf(name, sizeof name, "simple_opus_recording%d", i); + success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } else { + snprintf(name, sizeof name, "simple_aac_recording%d", i); + success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } + if (!success) + throw "Failed to create multi-track audio recording encoder " + "(simple output)"; + } + } +} + +#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" + +SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + + LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); + else + success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); + + if (!success) + throw "Failed to create audio streaming encoder (simple output)"; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + else + success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + + if (!success) + throw "Failed to create audio archive encoder (simple output)"; + + LoadRecordingPreset(); + + if (!ffmpegOutput) { + bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", + nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(simple output)"; + } + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); +} + +int SimpleOutput::GetAudioBitrate() const +{ + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); + + if (strcmp(audio_encoder, "opus") == 0) + return FindClosestAvailableSimpleOpusBitrate(bitrate); + + return FindClosestAvailableSimpleAACBitrate(bitrate); +} + +void SimpleOutput::Update() +{ + OBSDataAutoRelease videoSettings = obs_data_create(); + OBSDataAutoRelease audioSettings = obs_data_create(); + + int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); + int audioBitrate = GetAudioBitrate(); + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *encoder_id = obs_encoder_get_id(videoStreaming); + const char *presetType; + const char *preset; + + if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + presetType = "AMDPreset"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + presetType = "AMDPreset"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + presetType = "NVENCPreset2"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + presetType = "NVENCPreset2"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + presetType = "AMDAV1Preset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + presetType = "NVENCPreset2"; + + } else { + presetType = "Preset"; + } + + preset = config_get_string(main->Config(), "SimpleOutput", presetType); + + /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ + if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { + obs_data_set_string(videoSettings, "preset2", preset); + } else { + obs_data_set_string(videoSettings, "preset", preset); + } + + obs_data_set_string(videoSettings, "rate_control", "CBR"); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + + if (advanced) + obs_data_set_string(videoSettings, "x264opts", custom); + + obs_data_set_string(audioSettings, "rate_control", "CBR"); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + + obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); + + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + } + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, videoSettings); + obs_encoder_update(audioStreaming, audioSettings); + obs_encoder_update(audioArchive, audioSettings); +} + +void SimpleOutput::UpdateRecordingAudioSettings() +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", 192); + obs_data_set_string(settings, "rate_control", "CBR"); + + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv || strcmp(quality, "Stream") == 0) { + obs_encoder_update(audioRecording, settings); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_update(audioTrack[i], settings); + } + } + } +} + +#define CROSS_DIST_CUTOFF 2000.0 + +int SimpleOutput::CalcCRF(int crf) +{ + int cx = config_get_uint(main->Config(), "Video", "OutputCX"); + int cy = config_get_uint(main->Config(), "Video", "OutputCY"); + double fCX = double(cx); + double fCY = double(cy); + + if (lowCPUx264) + crf -= 2; + + double crossDist = sqrt(fCX * fCX + fCY * fCY); + double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; + crfResReduction = (1.0 - crfResReduction) * 10.0; + + return crf - int(crfResReduction); +} + +void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "crf", crf); + obs_data_set_bool(settings, "use_bufsize", true); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); + + obs_encoder_update(videoRecording, settings); +} + +static bool icq_available(obs_encoder_t *encoder) +{ + obs_properties_t *props = obs_encoder_properties(encoder); + obs_property_t *p = obs_properties_get(props, "rate_control"); + bool icq_found = false; + + size_t num = obs_property_list_item_count(p); + for (size_t i = 0; i < num; i++) { + const char *val = obs_property_list_item_string(p, i); + if (strcmp(val, "ICQ") == 0) { + icq_found = true; + break; + } + } + + obs_properties_destroy(props); + return icq_found; +} + +void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) +{ + bool icq = icq_available(videoRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "profile", "high"); + + if (icq && !av1) { + obs_data_set_string(settings, "rate_control", "ICQ"); + obs_data_set_int(settings, "icq_quality", crf); + } else { + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_int(settings, "cqp", crf); + } + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_apple(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} + +#ifdef ENABLE_HEVC +void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} +#endif + +void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", "quality"); + obs_data_set_int(settings, "cqp", cqp); + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings() +{ + bool ultra_hq = (videoQuality == "HQ"); + int crf = CalcCRF(ultra_hq ? 16 : 23); + + if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { + UpdateRecordingSettings_x264_crf(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV) { + UpdateRecordingSettings_qsv11(crf, false); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { + UpdateRecordingSettings_qsv11(crf, true); + + } else if (videoEncoder == SIMPLE_ENCODER_AMD) { + UpdateRecordingSettings_amd_cqp(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { + UpdateRecordingSettings_amd_cqp(crf); +#endif + + } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { + UpdateRecordingSettings_amd_cqp(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { + UpdateRecordingSettings_nvenc(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); +#endif + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { + /* These are magic numbers. 0 - 100, more is better. */ + UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { + UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); +#endif + } + UpdateRecordingAudioSettings(); +} + +inline void SimpleOutput::SetupOutputs() +{ + SimpleOutput::Update(); + obs_encoder_set_video(videoStreaming, obs_get_video()); + obs_encoder_set_audio(audioStreaming, obs_get_audio()); + obs_encoder_set_audio(audioArchive, obs_get_audio()); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (usingRecordingPreset) { + if (ffmpegOutput) { + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + } else { + obs_encoder_set_video(videoRecording, obs_get_video()); + if (flv) { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_set_audio(audioTrack[i], obs_get_audio()); + } + } + } + } + } else { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } +} + +const char *FindAudioEncoderFromCodec(const char *type) +{ + const char *alt_enc_id = nullptr; + size_t i = 0; + + while (obs_enum_encoder_types(i++, &alt_enc_id)) { + const char *codec = obs_get_encoder_codec(alt_enc_id); + if (strcmp(type, codec) == 0) { + return alt_enc_id; + } + } + + return nullptr; +} + +std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) +{ + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + auto audio_bitrate = GetAudioBitrate(); + auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", + OBSStreamStopping, this); + + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, + this); + stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); + obs_output_set_service(streamOutput, service); + return true; + }; + + return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, + [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +static inline bool ServiceSupportsVodTrack(const char *service); + +static void clear_archive_encoder(obs_output_t *output, const char *expected_name) +{ + obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); + bool clear = false; + + /* ensures that we don't remove twitch's soundtrack encoder */ + if (last) { + const char *name = obs_encoder_get_name(last); + clear = name && strcmp(name, expected_name) == 0; + obs_encoder_release(last); + } + + if (clear) + obs_output_set_audio_encoder(output, nullptr, 1); +} + +bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) +{ + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *name = obs_data_get_string(settings, "service"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) + return enableForCustomServer ? enable : false; + else + return advanced && enable && ServiceSupportsVodTrack(name); +} + +void SimpleOutput::SetupVodTrack(obs_service_t *service) +{ + if (IsVodTrackEnabled(service)) + obs_output_set_audio_encoder(streamOutput, audioArchive, 1); + else + clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); +} + +bool SimpleOutput::StartStreaming(obs_service_t *service) +{ + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_uint(main->Config(), "Output", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + if (!reconnect) + maxRetries = 0; + + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + + if (!multitrackVideo || !multitrackVideoActive) + SetupVodTrack(service); + + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +void SimpleOutput::UpdateRecording() +{ + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + int idx = 0; + int idx2 = 0; + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + + if (replayBufferActive || recordingActive) + return; + + if (usingRecordingPreset) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(streamOutput)) { + Update(); + } + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput) { + obs_output_set_video_encoder(fileOutput, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(fileOutput, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); + } + } + } + } + if (replayBuffer) { + obs_output_set_video_encoder(replayBuffer, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); + } + } + } + } + + recordingConfigured = true; +} + +bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) +{ + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); + int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + + bool is_fragmented = strncmp(format, "fragmented", 10) == 0; + bool is_lossless = videoQuality == "Lossless"; + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + if (updateReplayBuffer) { + f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(format); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); + } else { + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, + f.c_str(), ffmpegOutput); + obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); + if (ffmpegOutput) + obs_output_set_mixers(fileOutput, tracks); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented && !is_lossless) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + if (updateReplayBuffer) + obs_output_update(replayBuffer, settings); + else + obs_output_update(fileOutput, settings); + + return true; +} + +bool SimpleOutput::StartRecording() +{ + UpdateRecording(); + if (!ConfigureRecording(false)) + return false; + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool SimpleOutput::StartReplayBuffer() +{ + UpdateRecording(); + if (!ConfigureRecording(true)) + return false; + if (!obs_output_start(replayBuffer)) { + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); + return false; + } + + return true; +} + +void SimpleOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void SimpleOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void SimpleOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool SimpleOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool SimpleOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool SimpleOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +struct AdvancedOutput : BasicOutputHandler { + OBSEncoder streamAudioEnc; + OBSEncoder streamArchiveEnc; + OBSEncoder streamTrack[MAX_AUDIO_MIXES]; + OBSEncoder recordTrack[MAX_AUDIO_MIXES]; + OBSEncoder videoStreaming; + OBSEncoder videoRecording; + + bool ffmpegOutput; + bool ffmpegRecording; + bool useStreamEncoder; + bool useStreamAudioEncoder; + bool usesBitrate = false; + + AdvancedOutput(OBSBasic *main_); + + inline void UpdateStreamSettings(); + inline void UpdateRecordingSettings(); + inline void UpdateAudioSettings(); + virtual void Update() override; + + inline std::optional VodTrackMixerIdx(obs_service_t *service); + inline void SetupVodTrack(obs_service_t *service); + + inline void SetupStreaming(); + inline void SetupRecording(); + inline void SetupFFmpeg(); + void SetupOutputs() override; + int GetAudioBitrate(size_t i, const char *id) const; + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; + bool allowsMultiTrack(); +}; + +static OBSData GetDataFromJsonFile(const char *jsonFile) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); + + OBSDataAutoRelease data = nullptr; + + if (!jsonFilePath.empty()) { + BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); + + if (!!jsonData) { + data = obs_data_create_from_json(jsonData); + } + } + + if (!data) { + data = obs_data_create(); + } + + return data.Get(); +} + +static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) +{ + OBSData dataRet = obs_encoder_get_defaults(encoder); + obs_data_release(dataRet); + + if (!!settings) + obs_data_apply(dataRet, settings); + settings = std::move(dataRet); +} + +#define ADV_ARCHIVE_NAME "adv_archive_audio" + +#ifdef __APPLE__ +static void translate_macvth264_encoder(const char *&encoder) +{ + if (strcmp(encoder, "vt_h264_hw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; + } else if (strcmp(encoder, "vt_h264_sw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264"; + } +} +#endif + +AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); +#ifdef __APPLE__ + translate_macvth264_encoder(streamEncoder); + translate_macvth264_encoder(recordEncoder); +#endif + + ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); + useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; + useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; + + OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); + OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); + + if (ffmpegOutput) { + fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(advanced output)"; + } else { + bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, + nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(advanced output)"; + + if (!useStreamEncoder) { + videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", + recordEncSettings, nullptr); + if (!videoRecording) + throw "Failed to create recording video " + "encoder (advanced output)"; + obs_encoder_release(videoRecording); + } + } + + videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); + if (!videoStreaming) + throw "Failed to create streaming video encoder " + "(advanced output)"; + obs_encoder_release(videoStreaming); + + const char *rate_control = + obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); + if (!rate_control) + rate_control = ""; + usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || + astrcmpi(rate_control, "ABR") == 0; + + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[19]; + snprintf(name, sizeof(name), "adv_record_audio_%d", i); + + recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, + name, nullptr, i, nullptr); + + if (!recordTrack[i]) { + throw "Failed to create audio encoder " + "(advanced output)"; + } + + obs_encoder_release(recordTrack[i]); + + snprintf(name, sizeof(name), "adv_stream_audio_%d", i); + streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); + + if (!streamTrack[i]) { + throw "Failed to create streaming audio encoders " + "(advanced output)"; + } + + obs_encoder_release(streamTrack[i]); + } + + std::string id; + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + streamAudioEnc = + obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); + if (!streamAudioEnc) + throw "Failed to create streaming audio encoder " + "(advanced output)"; + obs_encoder_release(streamAudioEnc); + + id = ""; + int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; + streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); + if (!streamArchiveEnc) + throw "Failed to create archive audio encoder " + "(advanced output)"; + obs_encoder_release(streamArchiveEnc); + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); + recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, + this); +} + +void AdvancedOutput::UpdateStreamSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + + OBSData settings = GetDataFromJsonFile("streamEncoder.json"); + ApplyEncoderDefaults(settings, videoStreaming); + + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings, "bitrate"); + int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(settings, "bitrate", bitrate); + } + + int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) + obs_data_set_int(settings, "keyint_sec", keyint_sec); + } else { + blog(LOG_WARNING, "User is ignoring service settings."); + } + + if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) + obs_data_set_bool(settings, "lookahead", false); + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, settings); +} + +inline void AdvancedOutput::UpdateRecordingSettings() +{ + OBSData settings = GetDataFromJsonFile("recordEncoder.json"); + obs_encoder_update(videoRecording, settings); +} + +void AdvancedOutput::Update() +{ + UpdateStreamSettings(); + if (!useStreamEncoder && !ffmpegOutput) + UpdateRecordingSettings(); + UpdateAudioSettings(); +} + +static inline bool ServiceSupportsVodTrack(const char *service) +{ + static const char *vodTrackServices[] = {"Twitch"}; + + for (const char *vodTrackService : vodTrackServices) { + if (astrcmpi(vodTrackService, service) == 0) + return true; + } + + return false; +} + +inline bool AdvancedOutput::allowsMultiTrack() +{ + const char *protocol = nullptr; + obs_service_t *service_obj = main->GetService(); + protocol = obs_service_get_protocol(service_obj); + if (!protocol) + return false; + return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || + astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; +} + +inline void AdvancedOutput::SetupStreaming() +{ + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + bool is_multitrack_output = allowsMultiTrack(); + + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + obs_encoder_set_scaled_size(videoStreaming, cx, cy); + obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); + + const char *id = obs_service_get_id(main->GetService()); + if (strcmp(id, "rtmp_custom") == 0) { + OBSDataAutoRelease settings = obs_data_create(); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + obs_encoder_update(videoStreaming, settings); + } +} + +inline void AdvancedOutput::SetupRecording() +{ + const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); + const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); + int tracks; + + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); + + bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv) + tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); + else + tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); + + OBSDataAutoRelease settings = obs_data_create(); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + + /* Hack to allow recordings without any audio tracks selected. It is no + * longer possible to select such a configuration in settings, but legacy + * configurations might still have this configured and we don't want to + * just break them. */ + if (tracks == 0) + tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + + if (useStreamEncoder) { + obs_output_set_video_encoder(fileOutput, videoStreaming); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoStreaming); + } else { + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + obs_encoder_set_scaled_size(videoRecording, cx, cy); + obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); + obs_output_set_video_encoder(fileOutput, videoRecording); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoRecording); + } + + if (!flv) { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); + idx++; + } + } + } else if (flv && tracks != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); + + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + obs_data_set_string(settings, "path", path); + obs_output_update(fileOutput, settings); + if (replayBuffer) + obs_output_update(replayBuffer, settings); +} + +inline void AdvancedOutput::SetupFFmpeg() +{ + const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); + int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); + int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); + bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); + const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); + const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); + const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); + const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); + int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); + const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); + int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); + int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); + const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); + int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); + const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); + + OBSDataArrayAutoRelease audio_names = obs_data_array_create(); + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + + const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + OBSDataAutoRelease item = obs_data_create(); + obs_data_set_string(item, "name", audioName); + obs_data_array_push_back(audio_names, item); + } + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_array(settings, "audio_names", audio_names); + obs_data_set_string(settings, "url", url); + obs_data_set_string(settings, "format_name", formatName); + obs_data_set_string(settings, "format_mime_type", mimeType); + obs_data_set_string(settings, "muxer_settings", muxCustom); + obs_data_set_int(settings, "gop_size", gopSize); + obs_data_set_int(settings, "video_bitrate", vBitrate); + obs_data_set_string(settings, "video_encoder", vEncoder); + obs_data_set_int(settings, "video_encoder_id", vEncoderId); + obs_data_set_string(settings, "video_settings", vEncCustom); + obs_data_set_int(settings, "audio_bitrate", aBitrate); + obs_data_set_string(settings, "audio_encoder", aEncoder); + obs_data_set_int(settings, "audio_encoder_id", aEncoderId); + obs_data_set_string(settings, "audio_settings", aEncCustom); + + if (rescale && rescaleRes && *rescaleRes) { + int width; + int height; + int val = sscanf(rescaleRes, "%dx%d", &width, &height); + + if (val == 2 && width && height) { + obs_data_set_int(settings, "scale_width", width); + obs_data_set_int(settings, "scale_height", height); + } + } + + obs_output_set_mixers(fileOutput, aMixes); + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + obs_output_update(fileOutput, settings); +} + +static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) +{ + obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); +} + +inline void AdvancedOutput::UpdateAudioSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + + bool is_multitrack_output = allowsMultiTrack(); + + OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + string def_name = "Track"; + def_name += to_string((int)i + 1); + SetEncoderName(recordTrack[i], name, def_name.c_str()); + SetEncoderName(streamTrack[i], name, def_name.c_str()); + } + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + int track = (int)(i + 1); + settings[i] = obs_data_create(); + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); + + obs_encoder_update(recordTrack[i], settings[i]); + + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); + + if (!is_multitrack_output) { + if (track == streamTrackIndex || track == vodTrackIndex) { + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); + obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); + + if (!enforceBitrate) + obs_data_set_int(settings[i], "bitrate", bitrate); + } + } + + if (track == streamTrackIndex) + obs_encoder_update(streamAudioEnc, settings[i]); + if (track == vodTrackIndex) + obs_encoder_update(streamArchiveEnc, settings[i]); + } else { + obs_encoder_update(streamTrack[i], settings[i]); + } + } +} + +void AdvancedOutput::SetupOutputs() +{ + obs_encoder_set_video(videoStreaming, obs_get_video()); + if (videoRecording) + obs_encoder_set_video(videoRecording, obs_get_video()); + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + obs_encoder_set_audio(streamTrack[i], obs_get_audio()); + obs_encoder_set_audio(recordTrack[i], obs_get_audio()); + } + obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); + obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); + + SetupStreaming(); + + if (ffmpegOutput) + SetupFFmpeg(); + else + SetupRecording(); +} + +int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const +{ + static const char *names[] = { + "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", + }; + int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); + return FindClosestAvailableAudioBitrate(id, bitrate); +} + +inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) +{ + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) { + vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; + } else { + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *service = obs_data_get_string(settings, "service"); + if (!ServiceSupportsVodTrack(service)) + vodTrackEnabled = false; + } + + if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) + return {vodTrackIndex - 1}; + return std::nullopt; +} + +inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +{ + if (VodTrackMixerIdx(service).has_value()) + obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); + else + clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); +} + +std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) +{ + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + + bool is_multitrack_output = allowsMultiTrack(); + + if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", + OBSStreamStopping, this); + + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, + this); + stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + int idx = 0; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + return true; + }; + + return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), + VodTrackMixerIdx(service), [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +bool AdvancedOutput::StartStreaming(obs_service_t *service) +{ + obs_output_set_service(streamOutput, service); + + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_int(main->Config(), "Output", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + bool is_rtmp = false; + obs_service_t *service_obj = main->GetService(); + const char *protocol = obs_service_get_protocol(service_obj); + if (protocol) { + if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) + is_rtmp = true; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + if (!reconnect) + maxRetries = 0; + + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + if (is_rtmp) { + SetupVodTrack(service); + } + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +bool AdvancedOutput::StartRecording() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + bool splitFile; + const char *splitFileType; + int splitFileTime; + int splitFileSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) { + UpdateRecordingSettings(); + } + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); + + string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, + ffmpegRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); + + if (splitFile) { + splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); + splitFileTime = (astrcmpi(splitFileType, "Time") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") + : 0; + splitFileSize = (astrcmpi(splitFileType, "Size") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") + : 0; + string ext = GetFormatExt(recFormat); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", filenameFormat); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); + obs_data_set_bool(settings, "split_file", true); + obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); + obs_data_set_int(settings, "max_size_mb", splitFileSize); + } + + obs_output_update(fileOutput, settings); + } + + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool AdvancedOutput::StartReplayBuffer() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + const char *rbPrefix; + const char *rbSuffix; + int rbTime; + int rbSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); + rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + + string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(recFormat); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); + + obs_output_update(replayBuffer, settings); + } + + if (!obs_output_start(replayBuffer)) { + QString error_reason; + const char *error = obs_output_get_last_error(replayBuffer); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); + return false; + } + + return true; +} + +void AdvancedOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void AdvancedOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void AdvancedOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool AdvancedOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool AdvancedOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool AdvancedOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +void BasicOutputHandler::SetupAutoRemux(const char *&container) +{ + bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); + if (autoRemux && strcmp(container, "mp4") == 0) + container = "mkv"; +} + +std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, + bool overwrite, const char *format, bool ffmpeg) +{ + if (!ffmpeg) + SetupAutoRemux(container); + + string dst = GetOutputFilename(path, container, noSpace, overwrite, format); + lastRecordingPath = dst; + return dst; +} + +extern std::string DeserializeConfigText(const char *text); + +std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, + size_t main_audio_mixer, + std::optional vod_track_mixer, + std::function)> continuation) +{ + auto start_streaming_guard = std::make_shared(); + if (!multitrackVideo) { + continuation(std::nullopt); + return start_streaming_guard->GetFuture(); + } + + multitrackVideoActive = false; + + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; + + std::optional custom_config = std::nullopt; + if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) + custom_config = DeserializeConfigText( + config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + QString key = obs_data_get_string(settings, "key"); + + const char *service_name = ""; + if (is_custom && obs_data_has_user_value(settings, "service_name")) { + service_name = obs_data_get_string(settings, "service_name"); + } else if (!is_custom) { + service_name = obs_data_get_string(settings, "service"); + } + + std::optional custom_rtmp_url; + std::optional use_rtmps; + auto server = obs_data_get_string(settings, "server"); + if (strncmp(server, "auto", 4) != 0) { + custom_rtmp_url = server; + } else { + QString server_ = server; + use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); + } + + auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); + if (custom_rtmp_url.has_value()) { + blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); + } + + auto maximum_aggregate_bitrate = + config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") + ? std::nullopt + : std::make_optional( + config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); + + auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") + ? std::nullopt + : std::make_optional(config_get_int( + main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); + + auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); + + auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, + continuation = + std::move(continuation)](std::optional error) { + if (error) { + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + multitrackVideoActive = false; + if (!error->ShowDialog(main, multitrack_video_name)) + return continuation(false); + return continuation(std::nullopt); + } + + multitrackVideoActive = true; + + auto signal_handler = multitrackVideo->StreamingSignalHandler(); + + streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); + streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); + + startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); + stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); + return continuation(true); + }; + + QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), + service_name = std::string{service_name}, service = OBSService{service}, + stream_dump_config = OBSData{stream_dump_config}, + start_streaming_guard = start_streaming_guard]() mutable { + std::optional error; + try { + multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, + audio_encoder_id.c_str(), maximum_aggregate_bitrate, + maximum_video_tracks, custom_config, stream_dump_config, + main_audio_mixer, vod_track_mixer, use_rtmps); + } catch (const MultitrackVideoError &error_) { + error.emplace(error_); + } + + QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); + }); + + return start_streaming_guard->GetFuture(); +} + +OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() +{ + auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); + + if (!stream_dump_enabled) + return nullptr; + + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), + // never remux stream dump + false); + obs_data_set_string(settings, "path", strPath.c_str()); + + if (useMP4) { + obs_data_set_bool(settings, "use_mp4", true); + obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); + } + + return settings; +} + +BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) +{ + return new SimpleOutput(main); +} + +BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) +{ + return new AdvancedOutput(main); +} diff --git a/frontend/utility/BasicOutputHandler.cpp b/frontend/utility/BasicOutputHandler.cpp new file mode 100644 index 000000000..30d203174 --- /dev/null +++ b/frontend/utility/BasicOutputHandler.cpp @@ -0,0 +1,2541 @@ +#include +#include +#include +#include +#include +#include +#include "audio-encoders.hpp" +#include "multitrack-video-error.hpp" +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam.hpp" + +using namespace std; + +extern bool EncoderAvailable(const char *encoder); + +volatile bool streaming_active = false; +volatile bool recording_active = false; +volatile bool recording_paused = false; +volatile bool replaybuf_active = false; +volatile bool virtualcam_active = false; + +#define RTMP_PROTOCOL "rtmp" +#define SRT_PROTOCOL "srt" +#define RIST_PROTOCOL "rist" + +static void OBSStreamStarting(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + return; + + output->delayActive = true; + QMetaObject::invokeMethod(output->main, "StreamDelayStarting", Q_ARG(int, sec)); +} + +static void OBSStreamStopping(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + QMetaObject::invokeMethod(output->main, "StreamStopping"); + else + QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); +} + +static void OBSStartStreaming(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->streamingActive = true; + os_atomic_set_bool(&streaming_active, true); + QMetaObject::invokeMethod(output->main, "StreamingStart"); +} + +static void OBSStopStreaming(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->streamingActive = false; + output->delayActive = false; + output->multitrackVideoActive = false; + os_atomic_set_bool(&streaming_active, false); + QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSStartRecording(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->recordingActive = true; + os_atomic_set_bool(&recording_active, true); + QMetaObject::invokeMethod(output->main, "RecordingStart"); +} + +static void OBSStopRecording(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->recordingActive = false; + os_atomic_set_bool(&recording_active, false); + os_atomic_set_bool(&recording_paused, false); + QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSRecordStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "RecordStopping"); +} + +static void OBSRecordFileChanged(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + const char *next_file = calldata_string(params, "next_file"); + + QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); + + QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); + + output->lastRecordingPath = next_file; +} + +static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->replayBufferActive = true; + os_atomic_set_bool(&replaybuf_active, true); + QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); +} + +static void OBSStopReplayBuffer(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->replayBufferActive = false; + os_atomic_set_bool(&replaybuf_active, false); + QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); +} + +static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); +} + +static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); +} + +static void OBSStartVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->virtualCamActive = true; + os_atomic_set_bool(&virtualcam_active, true); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); +} + +static void OBSStopVirtualCam(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->virtualCamActive = false; + os_atomic_set_bool(&virtualcam_active, false); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); +} + +static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->DestroyVirtualCamView(); +} + +/* ------------------------------------------------------------------------ */ + +struct StartMultitrackVideoStreamingGuard { + StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; + ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } + + std::shared_future GetFuture() const { return future; } + + static std::shared_future MakeReadyFuture() + { + StartMultitrackVideoStreamingGuard guard; + return guard.GetFuture(); + } + +private: + std::promise guard; + std::shared_future future; +}; + +/* ------------------------------------------------------------------------ */ + +static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +static bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + +static const char *GetStreamOutputType(const obs_service_t *service) +{ + const char *protocol = obs_service_get_protocol(service); + const char *output = nullptr; + + if (!protocol) { + blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); + return nullptr; + } + + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); + return nullptr; + } + + /* Check if the service has a preferred output type */ + output = obs_service_get_preferred_output_type(service); + if (output) { + if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) + return output; + + blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); + } + + /* Otherwise, prefer first-party output types */ + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + return "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { + return "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { + return "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + obs_enum_output_types_with_protocol(protocol, &output, return_first_id); + if (output) + return output; + + blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); + + return nullptr; +} + +/* ------------------------------------------------------------------------ */ + +inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) +{ + if (main->vcamEnabled) { + virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); + + signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); + startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); + stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); + deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); + } + + auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); + if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { + auto service = main_->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); + } + if (multitrack_enabled) + multitrackVideo = make_unique(); +} + +extern void log_vcam_changed(const VCamConfig &config, bool starting); + +bool BasicOutputHandler::StartVirtualCam() +{ + if (!main->vcamEnabled) + return false; + + bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; + + if (!virtualCamView && !typeIsProgram) + virtualCamView = obs_view_create(); + + UpdateVirtualCamOutputSource(); + + if (!virtualCamVideo) { + virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); + + if (!virtualCamVideo) + return false; + } + + obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); + if (!Active()) + SetupOutputs(); + + bool success = obs_output_start(virtualCam); + if (!success) { + QString errorReason; + + const char *error = obs_output_get_last_error(virtualCam); + if (error) { + errorReason = QT_UTF8(error); + } else { + errorReason = QTStr("Output.StartFailedGeneric"); + } + + QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); + + DestroyVirtualCamView(); + } + + log_vcam_changed(main->vcamConfig, true); + + return success; +} + +void BasicOutputHandler::StopVirtualCam() +{ + if (main->vcamEnabled) { + obs_output_stop(virtualCam); + } +} + +bool BasicOutputHandler::VirtualCamActive() const +{ + if (main->vcamEnabled) { + return obs_output_active(virtualCam); + } + return false; +} + +void BasicOutputHandler::UpdateVirtualCamOutputSource() +{ + if (!main->vcamEnabled || !virtualCamView) + return; + + OBSSourceAutoRelease source; + + switch (main->vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + DestroyVirtualCameraScene(); + return; + case VCamOutputType::PreviewOutput: { + DestroyVirtualCameraScene(); + OBSSource s = main->GetCurrentSceneSource(); + obs_source_get_ref(s); + source = s.Get(); + break; + } + case VCamOutputType::SceneOutput: + DestroyVirtualCameraScene(); + source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); + + if (!vCamSourceScene) + vCamSourceScene = obs_scene_create_private("vcam_source"); + source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); + + if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { + obs_sceneitem_remove(vCamSourceSceneItem); + vCamSourceSceneItem = nullptr; + } + + if (!vCamSourceSceneItem) { + vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); + + obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); + obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); + + const struct vec2 size = { + (float)obs_source_get_width(source), + (float)obs_source_get_height(source), + }; + obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); + } + break; + } + + OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); + if (source != current) + obs_view_set_source(virtualCamView, 0, source); +} + +void BasicOutputHandler::DestroyVirtualCamView() +{ + if (main->vcamConfig.type == VCamOutputType::ProgramView) { + virtualCamVideo = nullptr; + return; + } + + obs_view_remove(virtualCamView); + obs_view_set_source(virtualCamView, 0, nullptr); + virtualCamVideo = nullptr; + + obs_view_destroy(virtualCamView); + virtualCamView = nullptr; + + DestroyVirtualCameraScene(); +} + +void BasicOutputHandler::DestroyVirtualCameraScene() +{ + if (!vCamSourceScene) + return; + + obs_scene_release(vCamSourceScene); + vCamSourceScene = nullptr; + vCamSourceSceneItem = nullptr; +} + +/* ------------------------------------------------------------------------ */ + +struct SimpleOutput : BasicOutputHandler { + OBSEncoder audioStreaming; + OBSEncoder videoStreaming; + OBSEncoder audioRecording; + OBSEncoder audioArchive; + OBSEncoder videoRecording; + OBSEncoder audioTrack[MAX_AUDIO_MIXES]; + + string videoEncoder; + string videoQuality; + bool usingRecordingPreset = false; + bool recordingConfigured = false; + bool ffmpegOutput = false; + bool lowCPUx264 = false; + + SimpleOutput(OBSBasic *main_); + + int CalcCRF(int crf); + + void UpdateRecordingSettings_x264_crf(int crf); + void UpdateRecordingSettings_qsv11(int crf, bool av1); + void UpdateRecordingSettings_nvenc(int cqp); + void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); + void UpdateRecordingSettings_amd_cqp(int cqp); + void UpdateRecordingSettings_apple(int quality); +#ifdef ENABLE_HEVC + void UpdateRecordingSettings_apple_hevc(int quality); +#endif + void UpdateRecordingSettings(); + void UpdateRecordingAudioSettings(); + virtual void Update() override; + + void SetupOutputs() override; + int GetAudioBitrate() const; + + void LoadRecordingPreset_Lossy(const char *encoder); + void LoadRecordingPreset_Lossless(); + void LoadRecordingPreset(); + + void LoadStreamingPreset_Lossy(const char *encoder); + + void UpdateRecording(); + bool ConfigureRecording(bool useReplayBuffer); + + bool IsVodTrackEnabled(obs_service_t *service); + void SetupVodTrack(obs_service_t *service); + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; +}; + +void SimpleOutput::LoadRecordingPreset_Lossless() +{ + fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(simple output)"; + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "format_name", "avi"); + obs_data_set_string(settings, "video_encoder", "utvideo"); + obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); + + obs_output_update(fileOutput, settings); +} + +void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) +{ + videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); + if (!videoRecording) + throw "Failed to create video recording encoder (simple output)"; + obs_encoder_release(videoRecording); +} + +void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) +{ + videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); + if (!videoStreaming) + throw "Failed to create video streaming encoder (simple output)"; + obs_encoder_release(videoStreaming); +} + +/* mistakes have been made to lead us to this. */ +const char *get_simple_output_encoder(const char *encoder) +{ + if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + return "obs_qsv11_v2"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + return "obs_qsv11_av1"; + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + return "h264_texture_amf"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + return "h265_texture_amf"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + return "av1_texture_amf"; + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + return "obs_nvenc_av1_tex"; + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.avc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.hevc"; +#endif + } + + return "obs_x264"; +} + +void SimpleOutput::LoadRecordingPreset() +{ + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); + + videoEncoder = encoder; + videoQuality = quality; + ffmpegOutput = false; + + if (strcmp(quality, "Stream") == 0) { + videoRecording = videoStreaming; + audioRecording = audioStreaming; + usingRecordingPreset = false; + return; + + } else if (strcmp(quality, "Lossless") == 0) { + LoadRecordingPreset_Lossless(); + usingRecordingPreset = true; + ffmpegOutput = true; + return; + + } else { + lowCPUx264 = false; + + if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) + lowCPUx264 = true; + LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); + usingRecordingPreset = true; + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); + else + success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); + + if (!success) + throw "Failed to create audio recording encoder " + "(simple output)"; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[23]; + if (strcmp(audio_encoder, "opus") == 0) { + snprintf(name, sizeof name, "simple_opus_recording%d", i); + success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } else { + snprintf(name, sizeof name, "simple_aac_recording%d", i); + success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } + if (!success) + throw "Failed to create multi-track audio recording encoder " + "(simple output)"; + } + } +} + +#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" + +SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + + LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); + else + success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); + + if (!success) + throw "Failed to create audio streaming encoder (simple output)"; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + else + success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + + if (!success) + throw "Failed to create audio archive encoder (simple output)"; + + LoadRecordingPreset(); + + if (!ffmpegOutput) { + bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", + nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(simple output)"; + } + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); +} + +int SimpleOutput::GetAudioBitrate() const +{ + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); + + if (strcmp(audio_encoder, "opus") == 0) + return FindClosestAvailableSimpleOpusBitrate(bitrate); + + return FindClosestAvailableSimpleAACBitrate(bitrate); +} + +void SimpleOutput::Update() +{ + OBSDataAutoRelease videoSettings = obs_data_create(); + OBSDataAutoRelease audioSettings = obs_data_create(); + + int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); + int audioBitrate = GetAudioBitrate(); + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *encoder_id = obs_encoder_get_id(videoStreaming); + const char *presetType; + const char *preset; + + if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + presetType = "AMDPreset"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + presetType = "AMDPreset"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + presetType = "NVENCPreset2"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + presetType = "NVENCPreset2"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + presetType = "AMDAV1Preset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + presetType = "NVENCPreset2"; + + } else { + presetType = "Preset"; + } + + preset = config_get_string(main->Config(), "SimpleOutput", presetType); + + /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ + if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { + obs_data_set_string(videoSettings, "preset2", preset); + } else { + obs_data_set_string(videoSettings, "preset", preset); + } + + obs_data_set_string(videoSettings, "rate_control", "CBR"); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + + if (advanced) + obs_data_set_string(videoSettings, "x264opts", custom); + + obs_data_set_string(audioSettings, "rate_control", "CBR"); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + + obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); + + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + } + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, videoSettings); + obs_encoder_update(audioStreaming, audioSettings); + obs_encoder_update(audioArchive, audioSettings); +} + +void SimpleOutput::UpdateRecordingAudioSettings() +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", 192); + obs_data_set_string(settings, "rate_control", "CBR"); + + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv || strcmp(quality, "Stream") == 0) { + obs_encoder_update(audioRecording, settings); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_update(audioTrack[i], settings); + } + } + } +} + +#define CROSS_DIST_CUTOFF 2000.0 + +int SimpleOutput::CalcCRF(int crf) +{ + int cx = config_get_uint(main->Config(), "Video", "OutputCX"); + int cy = config_get_uint(main->Config(), "Video", "OutputCY"); + double fCX = double(cx); + double fCY = double(cy); + + if (lowCPUx264) + crf -= 2; + + double crossDist = sqrt(fCX * fCX + fCY * fCY); + double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; + crfResReduction = (1.0 - crfResReduction) * 10.0; + + return crf - int(crfResReduction); +} + +void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "crf", crf); + obs_data_set_bool(settings, "use_bufsize", true); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); + + obs_encoder_update(videoRecording, settings); +} + +static bool icq_available(obs_encoder_t *encoder) +{ + obs_properties_t *props = obs_encoder_properties(encoder); + obs_property_t *p = obs_properties_get(props, "rate_control"); + bool icq_found = false; + + size_t num = obs_property_list_item_count(p); + for (size_t i = 0; i < num; i++) { + const char *val = obs_property_list_item_string(p, i); + if (strcmp(val, "ICQ") == 0) { + icq_found = true; + break; + } + } + + obs_properties_destroy(props); + return icq_found; +} + +void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) +{ + bool icq = icq_available(videoRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "profile", "high"); + + if (icq && !av1) { + obs_data_set_string(settings, "rate_control", "ICQ"); + obs_data_set_int(settings, "icq_quality", crf); + } else { + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_int(settings, "cqp", crf); + } + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_apple(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} + +#ifdef ENABLE_HEVC +void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} +#endif + +void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", "quality"); + obs_data_set_int(settings, "cqp", cqp); + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings() +{ + bool ultra_hq = (videoQuality == "HQ"); + int crf = CalcCRF(ultra_hq ? 16 : 23); + + if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { + UpdateRecordingSettings_x264_crf(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV) { + UpdateRecordingSettings_qsv11(crf, false); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { + UpdateRecordingSettings_qsv11(crf, true); + + } else if (videoEncoder == SIMPLE_ENCODER_AMD) { + UpdateRecordingSettings_amd_cqp(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { + UpdateRecordingSettings_amd_cqp(crf); +#endif + + } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { + UpdateRecordingSettings_amd_cqp(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { + UpdateRecordingSettings_nvenc(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); +#endif + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { + /* These are magic numbers. 0 - 100, more is better. */ + UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { + UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); +#endif + } + UpdateRecordingAudioSettings(); +} + +inline void SimpleOutput::SetupOutputs() +{ + SimpleOutput::Update(); + obs_encoder_set_video(videoStreaming, obs_get_video()); + obs_encoder_set_audio(audioStreaming, obs_get_audio()); + obs_encoder_set_audio(audioArchive, obs_get_audio()); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (usingRecordingPreset) { + if (ffmpegOutput) { + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + } else { + obs_encoder_set_video(videoRecording, obs_get_video()); + if (flv) { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_set_audio(audioTrack[i], obs_get_audio()); + } + } + } + } + } else { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } +} + +const char *FindAudioEncoderFromCodec(const char *type) +{ + const char *alt_enc_id = nullptr; + size_t i = 0; + + while (obs_enum_encoder_types(i++, &alt_enc_id)) { + const char *codec = obs_get_encoder_codec(alt_enc_id); + if (strcmp(type, codec) == 0) { + return alt_enc_id; + } + } + + return nullptr; +} + +std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) +{ + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + auto audio_bitrate = GetAudioBitrate(); + auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", + OBSStreamStopping, this); + + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, + this); + stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); + obs_output_set_service(streamOutput, service); + return true; + }; + + return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, + [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +static inline bool ServiceSupportsVodTrack(const char *service); + +static void clear_archive_encoder(obs_output_t *output, const char *expected_name) +{ + obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); + bool clear = false; + + /* ensures that we don't remove twitch's soundtrack encoder */ + if (last) { + const char *name = obs_encoder_get_name(last); + clear = name && strcmp(name, expected_name) == 0; + obs_encoder_release(last); + } + + if (clear) + obs_output_set_audio_encoder(output, nullptr, 1); +} + +bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) +{ + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *name = obs_data_get_string(settings, "service"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) + return enableForCustomServer ? enable : false; + else + return advanced && enable && ServiceSupportsVodTrack(name); +} + +void SimpleOutput::SetupVodTrack(obs_service_t *service) +{ + if (IsVodTrackEnabled(service)) + obs_output_set_audio_encoder(streamOutput, audioArchive, 1); + else + clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); +} + +bool SimpleOutput::StartStreaming(obs_service_t *service) +{ + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_uint(main->Config(), "Output", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + if (!reconnect) + maxRetries = 0; + + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + + if (!multitrackVideo || !multitrackVideoActive) + SetupVodTrack(service); + + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +void SimpleOutput::UpdateRecording() +{ + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + int idx = 0; + int idx2 = 0; + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + + if (replayBufferActive || recordingActive) + return; + + if (usingRecordingPreset) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(streamOutput)) { + Update(); + } + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput) { + obs_output_set_video_encoder(fileOutput, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(fileOutput, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); + } + } + } + } + if (replayBuffer) { + obs_output_set_video_encoder(replayBuffer, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); + } + } + } + } + + recordingConfigured = true; +} + +bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) +{ + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); + int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + + bool is_fragmented = strncmp(format, "fragmented", 10) == 0; + bool is_lossless = videoQuality == "Lossless"; + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + if (updateReplayBuffer) { + f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(format); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); + } else { + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, + f.c_str(), ffmpegOutput); + obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); + if (ffmpegOutput) + obs_output_set_mixers(fileOutput, tracks); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented && !is_lossless) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + if (updateReplayBuffer) + obs_output_update(replayBuffer, settings); + else + obs_output_update(fileOutput, settings); + + return true; +} + +bool SimpleOutput::StartRecording() +{ + UpdateRecording(); + if (!ConfigureRecording(false)) + return false; + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool SimpleOutput::StartReplayBuffer() +{ + UpdateRecording(); + if (!ConfigureRecording(true)) + return false; + if (!obs_output_start(replayBuffer)) { + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); + return false; + } + + return true; +} + +void SimpleOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void SimpleOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void SimpleOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool SimpleOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool SimpleOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool SimpleOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +struct AdvancedOutput : BasicOutputHandler { + OBSEncoder streamAudioEnc; + OBSEncoder streamArchiveEnc; + OBSEncoder streamTrack[MAX_AUDIO_MIXES]; + OBSEncoder recordTrack[MAX_AUDIO_MIXES]; + OBSEncoder videoStreaming; + OBSEncoder videoRecording; + + bool ffmpegOutput; + bool ffmpegRecording; + bool useStreamEncoder; + bool useStreamAudioEncoder; + bool usesBitrate = false; + + AdvancedOutput(OBSBasic *main_); + + inline void UpdateStreamSettings(); + inline void UpdateRecordingSettings(); + inline void UpdateAudioSettings(); + virtual void Update() override; + + inline std::optional VodTrackMixerIdx(obs_service_t *service); + inline void SetupVodTrack(obs_service_t *service); + + inline void SetupStreaming(); + inline void SetupRecording(); + inline void SetupFFmpeg(); + void SetupOutputs() override; + int GetAudioBitrate(size_t i, const char *id) const; + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; + bool allowsMultiTrack(); +}; + +static OBSData GetDataFromJsonFile(const char *jsonFile) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); + + OBSDataAutoRelease data = nullptr; + + if (!jsonFilePath.empty()) { + BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); + + if (!!jsonData) { + data = obs_data_create_from_json(jsonData); + } + } + + if (!data) { + data = obs_data_create(); + } + + return data.Get(); +} + +static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) +{ + OBSData dataRet = obs_encoder_get_defaults(encoder); + obs_data_release(dataRet); + + if (!!settings) + obs_data_apply(dataRet, settings); + settings = std::move(dataRet); +} + +#define ADV_ARCHIVE_NAME "adv_archive_audio" + +#ifdef __APPLE__ +static void translate_macvth264_encoder(const char *&encoder) +{ + if (strcmp(encoder, "vt_h264_hw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; + } else if (strcmp(encoder, "vt_h264_sw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264"; + } +} +#endif + +AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); +#ifdef __APPLE__ + translate_macvth264_encoder(streamEncoder); + translate_macvth264_encoder(recordEncoder); +#endif + + ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); + useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; + useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; + + OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); + OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); + + if (ffmpegOutput) { + fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(advanced output)"; + } else { + bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, + nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(advanced output)"; + + if (!useStreamEncoder) { + videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", + recordEncSettings, nullptr); + if (!videoRecording) + throw "Failed to create recording video " + "encoder (advanced output)"; + obs_encoder_release(videoRecording); + } + } + + videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); + if (!videoStreaming) + throw "Failed to create streaming video encoder " + "(advanced output)"; + obs_encoder_release(videoStreaming); + + const char *rate_control = + obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); + if (!rate_control) + rate_control = ""; + usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || + astrcmpi(rate_control, "ABR") == 0; + + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[19]; + snprintf(name, sizeof(name), "adv_record_audio_%d", i); + + recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, + name, nullptr, i, nullptr); + + if (!recordTrack[i]) { + throw "Failed to create audio encoder " + "(advanced output)"; + } + + obs_encoder_release(recordTrack[i]); + + snprintf(name, sizeof(name), "adv_stream_audio_%d", i); + streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); + + if (!streamTrack[i]) { + throw "Failed to create streaming audio encoders " + "(advanced output)"; + } + + obs_encoder_release(streamTrack[i]); + } + + std::string id; + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + streamAudioEnc = + obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); + if (!streamAudioEnc) + throw "Failed to create streaming audio encoder " + "(advanced output)"; + obs_encoder_release(streamAudioEnc); + + id = ""; + int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; + streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); + if (!streamArchiveEnc) + throw "Failed to create archive audio encoder " + "(advanced output)"; + obs_encoder_release(streamArchiveEnc); + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); + recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, + this); +} + +void AdvancedOutput::UpdateStreamSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + + OBSData settings = GetDataFromJsonFile("streamEncoder.json"); + ApplyEncoderDefaults(settings, videoStreaming); + + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings, "bitrate"); + int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(settings, "bitrate", bitrate); + } + + int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) + obs_data_set_int(settings, "keyint_sec", keyint_sec); + } else { + blog(LOG_WARNING, "User is ignoring service settings."); + } + + if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) + obs_data_set_bool(settings, "lookahead", false); + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, settings); +} + +inline void AdvancedOutput::UpdateRecordingSettings() +{ + OBSData settings = GetDataFromJsonFile("recordEncoder.json"); + obs_encoder_update(videoRecording, settings); +} + +void AdvancedOutput::Update() +{ + UpdateStreamSettings(); + if (!useStreamEncoder && !ffmpegOutput) + UpdateRecordingSettings(); + UpdateAudioSettings(); +} + +static inline bool ServiceSupportsVodTrack(const char *service) +{ + static const char *vodTrackServices[] = {"Twitch"}; + + for (const char *vodTrackService : vodTrackServices) { + if (astrcmpi(vodTrackService, service) == 0) + return true; + } + + return false; +} + +inline bool AdvancedOutput::allowsMultiTrack() +{ + const char *protocol = nullptr; + obs_service_t *service_obj = main->GetService(); + protocol = obs_service_get_protocol(service_obj); + if (!protocol) + return false; + return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || + astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; +} + +inline void AdvancedOutput::SetupStreaming() +{ + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + bool is_multitrack_output = allowsMultiTrack(); + + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + obs_encoder_set_scaled_size(videoStreaming, cx, cy); + obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); + + const char *id = obs_service_get_id(main->GetService()); + if (strcmp(id, "rtmp_custom") == 0) { + OBSDataAutoRelease settings = obs_data_create(); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + obs_encoder_update(videoStreaming, settings); + } +} + +inline void AdvancedOutput::SetupRecording() +{ + const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); + const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); + int tracks; + + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); + + bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv) + tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); + else + tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); + + OBSDataAutoRelease settings = obs_data_create(); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + + /* Hack to allow recordings without any audio tracks selected. It is no + * longer possible to select such a configuration in settings, but legacy + * configurations might still have this configured and we don't want to + * just break them. */ + if (tracks == 0) + tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + + if (useStreamEncoder) { + obs_output_set_video_encoder(fileOutput, videoStreaming); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoStreaming); + } else { + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + obs_encoder_set_scaled_size(videoRecording, cx, cy); + obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); + obs_output_set_video_encoder(fileOutput, videoRecording); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoRecording); + } + + if (!flv) { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); + idx++; + } + } + } else if (flv && tracks != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); + + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + obs_data_set_string(settings, "path", path); + obs_output_update(fileOutput, settings); + if (replayBuffer) + obs_output_update(replayBuffer, settings); +} + +inline void AdvancedOutput::SetupFFmpeg() +{ + const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); + int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); + int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); + bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); + const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); + const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); + const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); + const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); + int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); + const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); + int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); + int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); + const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); + int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); + const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); + + OBSDataArrayAutoRelease audio_names = obs_data_array_create(); + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + + const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + OBSDataAutoRelease item = obs_data_create(); + obs_data_set_string(item, "name", audioName); + obs_data_array_push_back(audio_names, item); + } + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_array(settings, "audio_names", audio_names); + obs_data_set_string(settings, "url", url); + obs_data_set_string(settings, "format_name", formatName); + obs_data_set_string(settings, "format_mime_type", mimeType); + obs_data_set_string(settings, "muxer_settings", muxCustom); + obs_data_set_int(settings, "gop_size", gopSize); + obs_data_set_int(settings, "video_bitrate", vBitrate); + obs_data_set_string(settings, "video_encoder", vEncoder); + obs_data_set_int(settings, "video_encoder_id", vEncoderId); + obs_data_set_string(settings, "video_settings", vEncCustom); + obs_data_set_int(settings, "audio_bitrate", aBitrate); + obs_data_set_string(settings, "audio_encoder", aEncoder); + obs_data_set_int(settings, "audio_encoder_id", aEncoderId); + obs_data_set_string(settings, "audio_settings", aEncCustom); + + if (rescale && rescaleRes && *rescaleRes) { + int width; + int height; + int val = sscanf(rescaleRes, "%dx%d", &width, &height); + + if (val == 2 && width && height) { + obs_data_set_int(settings, "scale_width", width); + obs_data_set_int(settings, "scale_height", height); + } + } + + obs_output_set_mixers(fileOutput, aMixes); + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + obs_output_update(fileOutput, settings); +} + +static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) +{ + obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); +} + +inline void AdvancedOutput::UpdateAudioSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + + bool is_multitrack_output = allowsMultiTrack(); + + OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + string def_name = "Track"; + def_name += to_string((int)i + 1); + SetEncoderName(recordTrack[i], name, def_name.c_str()); + SetEncoderName(streamTrack[i], name, def_name.c_str()); + } + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + int track = (int)(i + 1); + settings[i] = obs_data_create(); + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); + + obs_encoder_update(recordTrack[i], settings[i]); + + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); + + if (!is_multitrack_output) { + if (track == streamTrackIndex || track == vodTrackIndex) { + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); + obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); + + if (!enforceBitrate) + obs_data_set_int(settings[i], "bitrate", bitrate); + } + } + + if (track == streamTrackIndex) + obs_encoder_update(streamAudioEnc, settings[i]); + if (track == vodTrackIndex) + obs_encoder_update(streamArchiveEnc, settings[i]); + } else { + obs_encoder_update(streamTrack[i], settings[i]); + } + } +} + +void AdvancedOutput::SetupOutputs() +{ + obs_encoder_set_video(videoStreaming, obs_get_video()); + if (videoRecording) + obs_encoder_set_video(videoRecording, obs_get_video()); + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + obs_encoder_set_audio(streamTrack[i], obs_get_audio()); + obs_encoder_set_audio(recordTrack[i], obs_get_audio()); + } + obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); + obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); + + SetupStreaming(); + + if (ffmpegOutput) + SetupFFmpeg(); + else + SetupRecording(); +} + +int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const +{ + static const char *names[] = { + "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", + }; + int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); + return FindClosestAvailableAudioBitrate(id, bitrate); +} + +inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) +{ + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) { + vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; + } else { + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *service = obs_data_get_string(settings, "service"); + if (!ServiceSupportsVodTrack(service)) + vodTrackEnabled = false; + } + + if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) + return {vodTrackIndex - 1}; + return std::nullopt; +} + +inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +{ + if (VodTrackMixerIdx(service).has_value()) + obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); + else + clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); +} + +std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) +{ + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + + bool is_multitrack_output = allowsMultiTrack(); + + if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", + OBSStreamStopping, this); + + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, + this); + stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + int idx = 0; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + return true; + }; + + return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), + VodTrackMixerIdx(service), [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +bool AdvancedOutput::StartStreaming(obs_service_t *service) +{ + obs_output_set_service(streamOutput, service); + + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_int(main->Config(), "Output", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + bool is_rtmp = false; + obs_service_t *service_obj = main->GetService(); + const char *protocol = obs_service_get_protocol(service_obj); + if (protocol) { + if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) + is_rtmp = true; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + if (!reconnect) + maxRetries = 0; + + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + if (is_rtmp) { + SetupVodTrack(service); + } + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +bool AdvancedOutput::StartRecording() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + bool splitFile; + const char *splitFileType; + int splitFileTime; + int splitFileSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) { + UpdateRecordingSettings(); + } + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); + + string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, + ffmpegRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); + + if (splitFile) { + splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); + splitFileTime = (astrcmpi(splitFileType, "Time") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") + : 0; + splitFileSize = (astrcmpi(splitFileType, "Size") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") + : 0; + string ext = GetFormatExt(recFormat); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", filenameFormat); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); + obs_data_set_bool(settings, "split_file", true); + obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); + obs_data_set_int(settings, "max_size_mb", splitFileSize); + } + + obs_output_update(fileOutput, settings); + } + + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool AdvancedOutput::StartReplayBuffer() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + const char *rbPrefix; + const char *rbSuffix; + int rbTime; + int rbSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); + rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + + string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(recFormat); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); + + obs_output_update(replayBuffer, settings); + } + + if (!obs_output_start(replayBuffer)) { + QString error_reason; + const char *error = obs_output_get_last_error(replayBuffer); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); + return false; + } + + return true; +} + +void AdvancedOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void AdvancedOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void AdvancedOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool AdvancedOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool AdvancedOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool AdvancedOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +void BasicOutputHandler::SetupAutoRemux(const char *&container) +{ + bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); + if (autoRemux && strcmp(container, "mp4") == 0) + container = "mkv"; +} + +std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, + bool overwrite, const char *format, bool ffmpeg) +{ + if (!ffmpeg) + SetupAutoRemux(container); + + string dst = GetOutputFilename(path, container, noSpace, overwrite, format); + lastRecordingPath = dst; + return dst; +} + +extern std::string DeserializeConfigText(const char *text); + +std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, + size_t main_audio_mixer, + std::optional vod_track_mixer, + std::function)> continuation) +{ + auto start_streaming_guard = std::make_shared(); + if (!multitrackVideo) { + continuation(std::nullopt); + return start_streaming_guard->GetFuture(); + } + + multitrackVideoActive = false; + + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; + + std::optional custom_config = std::nullopt; + if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) + custom_config = DeserializeConfigText( + config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + QString key = obs_data_get_string(settings, "key"); + + const char *service_name = ""; + if (is_custom && obs_data_has_user_value(settings, "service_name")) { + service_name = obs_data_get_string(settings, "service_name"); + } else if (!is_custom) { + service_name = obs_data_get_string(settings, "service"); + } + + std::optional custom_rtmp_url; + std::optional use_rtmps; + auto server = obs_data_get_string(settings, "server"); + if (strncmp(server, "auto", 4) != 0) { + custom_rtmp_url = server; + } else { + QString server_ = server; + use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); + } + + auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); + if (custom_rtmp_url.has_value()) { + blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); + } + + auto maximum_aggregate_bitrate = + config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") + ? std::nullopt + : std::make_optional( + config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); + + auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") + ? std::nullopt + : std::make_optional(config_get_int( + main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); + + auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); + + auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, + continuation = + std::move(continuation)](std::optional error) { + if (error) { + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + multitrackVideoActive = false; + if (!error->ShowDialog(main, multitrack_video_name)) + return continuation(false); + return continuation(std::nullopt); + } + + multitrackVideoActive = true; + + auto signal_handler = multitrackVideo->StreamingSignalHandler(); + + streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); + streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); + + startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); + stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); + return continuation(true); + }; + + QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), + service_name = std::string{service_name}, service = OBSService{service}, + stream_dump_config = OBSData{stream_dump_config}, + start_streaming_guard = start_streaming_guard]() mutable { + std::optional error; + try { + multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, + audio_encoder_id.c_str(), maximum_aggregate_bitrate, + maximum_video_tracks, custom_config, stream_dump_config, + main_audio_mixer, vod_track_mixer, use_rtmps); + } catch (const MultitrackVideoError &error_) { + error.emplace(error_); + } + + QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); + }); + + return start_streaming_guard->GetFuture(); +} + +OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() +{ + auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); + + if (!stream_dump_enabled) + return nullptr; + + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), + // never remux stream dump + false); + obs_data_set_string(settings, "path", strPath.c_str()); + + if (useMP4) { + obs_data_set_bool(settings, "use_mp4", true); + obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); + } + + return settings; +} + +BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) +{ + return new SimpleOutput(main); +} + +BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) +{ + return new AdvancedOutput(main); +} diff --git a/UI/window-basic-main-outputs.hpp b/frontend/utility/BasicOutputHandler.hpp similarity index 100% rename from UI/window-basic-main-outputs.hpp rename to frontend/utility/BasicOutputHandler.hpp diff --git a/UI/window-basic-main-transitions.cpp b/frontend/utility/QuickTransition.cpp similarity index 100% rename from UI/window-basic-main-transitions.cpp rename to frontend/utility/QuickTransition.cpp diff --git a/UI/window-basic-main.hpp b/frontend/utility/QuickTransition.hpp similarity index 100% rename from UI/window-basic-main.hpp rename to frontend/utility/QuickTransition.hpp diff --git a/UI/window-basic-main.cpp b/frontend/utility/SceneRenameDelegate.cpp similarity index 100% rename from UI/window-basic-main.cpp rename to frontend/utility/SceneRenameDelegate.cpp diff --git a/frontend/utility/SceneRenameDelegate.hpp b/frontend/utility/SceneRenameDelegate.hpp new file mode 100644 index 000000000..81bb7b478 --- /dev/null +++ b/frontend/utility/SceneRenameDelegate.hpp @@ -0,0 +1,1382 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "window-main.hpp" +#include "window-basic-interaction.hpp" +#include "window-basic-vcam.hpp" +#include "window-basic-properties.hpp" +#include "window-basic-transform.hpp" +#include "window-basic-adv-audio.hpp" +#include "window-basic-filters.hpp" +#include "window-missing-files.hpp" +#include "window-projector.hpp" +#include "window-basic-about.hpp" +#ifdef YOUTUBE_ENABLED +#include "window-dock-youtube-app.hpp" +#endif +#include "auth-base.hpp" +#include "log-viewer.hpp" +#include "undo-stack-obs.hpp" + +#include + +#include +#include +#include + +#include + +class QMessageBox; +class QListWidgetItem; +class VolControl; +class OBSBasicStats; +class OBSBasicVCamConfig; + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#define DESKTOP_AUDIO_1 Str("DesktopAudioDevice1") +#define DESKTOP_AUDIO_2 Str("DesktopAudioDevice2") +#define AUX_AUDIO_1 Str("AuxAudioDevice1") +#define AUX_AUDIO_2 Str("AuxAudioDevice2") +#define AUX_AUDIO_3 Str("AuxAudioDevice3") +#define AUX_AUDIO_4 Str("AuxAudioDevice4") + +#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" + +#define PREVIEW_EDGE_SIZE 10 + +struct BasicOutputHandler; + +enum class QtDataRole { + OBSRef = Qt::UserRole, + OBSSignals, +}; + +struct SavedProjectorInfo { + ProjectorType type; + int monitor; + std::string geometry; + std::string name; + bool alwaysOnTop; + bool alwaysOnTopOverridden; +}; + +struct SourceCopyInfo { + OBSWeakSource weak_source; + bool visible; + obs_sceneitem_crop crop; + obs_transform_info transform; + obs_blending_method blend_method; + obs_blending_type blend_mode; +}; + +struct QuickTransition { + QPushButton *button = nullptr; + OBSSource source; + obs_hotkey_id hotkey = OBS_INVALID_HOTKEY_ID; + int duration = 0; + int id = 0; + bool fadeToBlack = false; + + inline QuickTransition() {} + inline QuickTransition(OBSSource source_, int duration_, int id_, bool fadeToBlack_ = false) + : source(source_), + duration(duration_), + id(id_), + fadeToBlack(fadeToBlack_), + renamedSignal(std::make_shared(obs_source_get_signal_handler(source), "rename", + SourceRenamed, this)) + { + } + +private: + static void SourceRenamed(void *param, calldata_t *data); + std::shared_ptr renamedSignal; +}; + +struct OBSProfile { + std::string name; + std::string directoryName; + std::filesystem::path path; + std::filesystem::path profileFile; +}; + +struct OBSSceneCollection { + std::string name; + std::string fileName; + std::filesystem::path collectionFile; +}; + +struct OBSPromptResult { + bool success; + std::string promptValue; + bool optionValue; +}; + +struct OBSPromptRequest { + std::string title; + std::string prompt; + std::string promptValue; + bool withOption; + std::string optionPrompt; + bool optionValue; +}; + +using OBSPromptCallback = std::function; + +using OBSProfileCache = std::map; +using OBSSceneCollectionCache = std::map; + +class ColorSelect : public QWidget { + +public: + explicit ColorSelect(QWidget *parent = 0); + +private: + std::unique_ptr ui; +}; + +class OBSBasic : public OBSMainWindow { + Q_OBJECT + Q_PROPERTY(QIcon imageIcon READ GetImageIcon WRITE SetImageIcon DESIGNABLE true) + Q_PROPERTY(QIcon colorIcon READ GetColorIcon WRITE SetColorIcon DESIGNABLE true) + Q_PROPERTY(QIcon slideshowIcon READ GetSlideshowIcon WRITE SetSlideshowIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioInputIcon READ GetAudioInputIcon WRITE SetAudioInputIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioOutputIcon READ GetAudioOutputIcon WRITE SetAudioOutputIcon DESIGNABLE true) + Q_PROPERTY(QIcon desktopCapIcon READ GetDesktopCapIcon WRITE SetDesktopCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon windowCapIcon READ GetWindowCapIcon WRITE SetWindowCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon gameCapIcon READ GetGameCapIcon WRITE SetGameCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon cameraIcon READ GetCameraIcon WRITE SetCameraIcon DESIGNABLE true) + Q_PROPERTY(QIcon textIcon READ GetTextIcon WRITE SetTextIcon DESIGNABLE true) + Q_PROPERTY(QIcon mediaIcon READ GetMediaIcon WRITE SetMediaIcon DESIGNABLE true) + Q_PROPERTY(QIcon browserIcon READ GetBrowserIcon WRITE SetBrowserIcon DESIGNABLE true) + Q_PROPERTY(QIcon groupIcon READ GetGroupIcon WRITE SetGroupIcon DESIGNABLE true) + Q_PROPERTY(QIcon sceneIcon READ GetSceneIcon WRITE SetSceneIcon DESIGNABLE true) + Q_PROPERTY(QIcon defaultIcon READ GetDefaultIcon WRITE SetDefaultIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioProcessOutputIcon READ GetAudioProcessOutputIcon WRITE SetAudioProcessOutputIcon + DESIGNABLE true) + + friend class OBSAbout; + friend class OBSBasicPreview; + friend class OBSBasicStatusBar; + friend class OBSBasicSourceSelect; + friend class OBSBasicTransform; + friend class OBSBasicSettings; + friend class Auth; + friend class AutoConfig; + friend class AutoConfigStreamPage; + friend class RecordButton; + friend class ControlsSplitButton; + friend class ExtraBrowsersModel; + friend class ExtraBrowsersDelegate; + friend class DeviceCaptureToolbar; + friend class OBSBasicSourceSelect; + friend class OBSYoutubeActions; + friend class OBSPermissions; + friend struct BasicOutputHandler; + friend struct OBSStudioAPI; + friend class ScreenshotObj; + + enum class MoveDir { Up, Down, Left, Right }; + + enum DropType { + DropType_RawText, + DropType_Text, + DropType_Image, + DropType_Media, + DropType_Html, + DropType_Url, + }; + + enum ContextBarSize { ContextBarSize_Minimized, ContextBarSize_Reduced, ContextBarSize_Normal }; + + enum class CenterType { + Scene, + Vertical, + Horizontal, + }; + +private: + obs_frontend_callbacks *api = nullptr; + + std::shared_ptr auth; + + std::vector volumes; + + std::vector signalHandlers; + + QList> oldExtraDocks; + QStringList oldExtraDockNames; + + OBSDataAutoRelease collectionModuleData; + std::vector safeModeTransitions; + + bool loaded = false; + long disableSaving = 1; + bool projectChanged = false; + bool previewEnabled = true; + ContextBarSize contextBarSize = ContextBarSize_Normal; + + std::deque clipboard; + OBSWeakSourceAutoRelease copyFiltersSource; + bool copyVisible = true; + obs_transform_info copiedTransformInfo; + obs_sceneitem_crop copiedCropInfo; + bool hasCopiedTransform = false; + OBSWeakSourceAutoRelease copySourceTransition; + int copySourceTransitionDuration; + + bool closing = false; + bool clearingFailed = false; + + QScopedPointer devicePropertiesThread; + QScopedPointer whatsNewInitThread; + QScopedPointer updateCheckThread; + QScopedPointer introCheckThread; + QScopedPointer logUploadThread; + + QPointer interaction; + QPointer properties; + QPointer transformWindow; + QPointer advAudioWindow; + QPointer filters; + QPointer statsDock; +#ifdef YOUTUBE_ENABLED + QPointer youtubeAppDock; + uint64_t lastYouTubeAppDockCreationTime = 0; +#endif + QPointer about; + QPointer missDialog; + QPointer logView; + + QPointer cpuUsageTimer; + QPointer diskFullTimer; + + QPointer nudge_timer; + bool recent_nudge = false; + + os_cpu_usage_info_t *cpuUsageInfo = nullptr; + + OBSService service; + std::unique_ptr outputHandler; + std::shared_future setupStreamingGuard; + bool streamingStopping = false; + bool recordingStopping = false; + bool replayBufferStopping = false; + + gs_vertbuffer_t *box = nullptr; + gs_vertbuffer_t *boxLeft = nullptr; + gs_vertbuffer_t *boxTop = nullptr; + gs_vertbuffer_t *boxRight = nullptr; + gs_vertbuffer_t *boxBottom = nullptr; + gs_vertbuffer_t *circle = nullptr; + + gs_vertbuffer_t *actionSafeMargin = nullptr; + gs_vertbuffer_t *graphicsSafeMargin = nullptr; + gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; + gs_vertbuffer_t *leftLine = nullptr; + gs_vertbuffer_t *topLine = nullptr; + gs_vertbuffer_t *rightLine = nullptr; + + int previewX = 0, previewY = 0; + int previewCX = 0, previewCY = 0; + float previewScale = 0.0f; + + ConfigFile activeConfiguration; + + std::vector savedProjectorsArray; + std::vector projectors; + + QPointer stats; + QPointer remux; + QPointer extraBrowsers; + QPointer importer; + + QPointer transitionButton; + + bool vcamEnabled = false; + VCamConfig vcamConfig; + + QScopedPointer trayIcon; + QPointer sysTrayStream; + QPointer sysTrayRecord; + QPointer sysTrayReplayBuffer; + QPointer sysTrayVirtualCam; + QPointer showHide; + QPointer exit; + QPointer trayMenu; + QPointer previewProjector; + QPointer studioProgramProjector; + QPointer previewProjectorSource; + QPointer previewProjectorMain; + QPointer sceneProjectorMenu; + QPointer sourceProjector; + QPointer scaleFilteringMenu; + QPointer blendingMethodMenu; + QPointer blendingModeMenu; + QPointer colorMenu; + QPointer colorWidgetAction; + QPointer colorSelect; + QPointer deinterlaceMenu; + QPointer perSceneTransitionMenu; + QPointer shortcutFilter; + QPointer renameScene; + QPointer renameSource; + + QPointer programWidget; + QPointer programLayout; + QPointer programLabel; + + QScopedPointer patronJsonThread; + std::string patronJson; + + std::atomic currentScene = nullptr; + std::optional> lastOutputResolution; + std::optional> migrationBaseResolution; + bool usingAbsoluteCoordinates = false; + + void DisableRelativeCoordinates(bool disable); + + void OnEvent(enum obs_frontend_event event); + + void UpdateMultiviewProjectorMenu(); + + void DrawBackdrop(float cx, float cy); + + void SetupEncoders(); + + void CreateFirstRunSources(); + void CreateDefaultScene(bool firstStart); + + void UpdateVolumeControlsDecayRate(); + void UpdateVolumeControlsPeakMeterType(); + void ClearVolumeControls(); + + void UploadLog(const char *subdir, const char *file, const bool crash); + + void Save(const char *file); + void LoadData(obs_data_t *data, const char *file, bool remigrate = false); + void Load(const char *file, bool remigrate = false); + + void InitHotkeys(); + void CreateHotkeys(); + void ClearHotkeys(); + + bool InitService(); + + bool InitBasicConfigDefaults(); + void InitBasicConfigDefaults2(); + bool InitBasicConfig(); + + void InitOBSCallbacks(); + + void InitPrimitives(); + + void OnFirstLoad(); + + OBSSceneItem GetSceneItem(QListWidgetItem *item); + OBSSceneItem GetCurrentSceneItem(); + + bool QueryRemoveSource(obs_source_t *source); + + void TimedCheckForUpdates(); + void CheckForUpdates(bool manualUpdate); + + void GetFPSCommon(uint32_t &num, uint32_t &den) const; + void GetFPSInteger(uint32_t &num, uint32_t &den) const; + void GetFPSFraction(uint32_t &num, uint32_t &den) const; + void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const; + void GetConfigFPS(uint32_t &num, uint32_t &den) const; + + void UpdatePreviewScalingMenu(); + + void LoadSceneListOrder(obs_data_array_t *array); + obs_data_array_t *SaveSceneListOrder(); + void ChangeSceneIndex(bool relative, int idx, int invalidIdx); + + void TempFileOutput(const char *path, int vBitrate, int aBitrate); + void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); + + void CloseDialogs(); + void ClearSceneData(); + void ClearProjectors(); + + void Nudge(int dist, MoveDir dir); + + OBSProjector *OpenProjector(obs_source_t *source, int monitor, ProjectorType type); + + void GetAudioSourceFilters(); + void GetAudioSourceProperties(); + void VolControlContextMenu(); + void ToggleVolControlLayout(); + void ToggleMixerLayout(bool vertical); + + void LogScenes(); + void SaveProjectNow(); + + int GetTopSelectedSourceItem(); + + QModelIndexList GetAllSelectedSourceItems(); + + obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, replayBufHotkeys, vcamHotkeys, + togglePreviewHotkeys, contextBarHotkeys; + obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey, addChapterHotkey; + + void InitDefaultTransitions(); + void InitTransition(obs_source_t *transition); + obs_source_t *FindTransition(const char *name); + OBSSource GetCurrentTransition(); + obs_data_array_t *SaveTransitions(); + void LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data); + + obs_source_t *fadeTransition; + obs_source_t *cutTransition; + + void CreateProgramDisplay(); + void CreateProgramOptions(); + void AddQuickTransitionId(int id); + void AddQuickTransition(); + void AddQuickTransitionHotkey(QuickTransition *qt); + void RemoveQuickTransitionHotkey(QuickTransition *qt); + void LoadQuickTransitions(obs_data_array_t *array); + obs_data_array_t *SaveQuickTransitions(); + void ClearQuickTransitionWidgets(); + void RefreshQuickTransitions(); + void DisableQuickTransitionWidgets(); + void EnableTransitionWidgets(bool enable); + void CreateDefaultQuickTransitions(); + + void PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration); + QMenu *CreatePerSceneTransitionMenu(); + QMenu *CreateVisibilityTransitionMenu(bool visible); + + QuickTransition *GetQuickTransition(int id); + int GetQuickTransitionIdx(int id); + QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt); + void ClearQuickTransitions(); + void QuickTransitionClicked(); + void QuickTransitionChange(); + void QuickTransitionChangeDuration(int value); + void QuickTransitionRemoveClicked(); + + void SetPreviewProgramMode(bool enabled); + void ResizeProgram(uint32_t cx, uint32_t cy); + void SetCurrentScene(obs_scene_t *scene, bool force = false); + static void RenderProgram(void *data, uint32_t cx, uint32_t cy); + + std::vector quickTransitions; + QPointer programOptions; + QPointer program; + OBSWeakSource lastScene; + OBSWeakSource swapScene; + OBSWeakSource programScene; + OBSWeakSource lastProgramScene; + bool editPropertiesMode = false; + bool sceneDuplicationMode = true; + bool swapScenesMode = true; + volatile bool previewProgramMode = false; + obs_hotkey_pair_id togglePreviewProgramHotkeys = 0; + obs_hotkey_id transitionHotkey = 0; + obs_hotkey_id statsHotkey = 0; + obs_hotkey_id screenshotHotkey = 0; + obs_hotkey_id sourceScreenshotHotkey = 0; + int quickTransitionIdCounter = 1; + bool overridingTransition = false; + + int programX = 0, programY = 0; + int programCX = 0, programCY = 0; + float programScale = 0.0f; + + int disableOutputsRef = 0; + + inline void OnActivate(bool force = false); + inline void OnDeactivate(); + + void AddDropSource(const char *file, DropType image); + void AddDropURL(const char *url, QString &name, obs_data_t *settings, const obs_video_info &ovi); + void ConfirmDropUrl(const QString &url); + void dragEnterEvent(QDragEnterEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; + + bool sysTrayMinimizeToTray(); + + void EnumDialogs(); + + QList visDialogs; + QList modalDialogs; + QList visMsgBoxes; + + QList visDlgPositions; + + QByteArray startingDockLayout; + + obs_data_array_t *SaveProjectors(); + void LoadSavedProjectors(obs_data_array_t *savedProjectors); + + void MacBranchesFetched(const QString &branch, bool manualUpdate); + void ReceivedIntroJson(const QString &text); + void ShowWhatsNew(const QString &url); + + void UpdatePreviewProgramIndicators(); + + QStringList extraDockNames; + QList> extraDocks; + + QStringList extraCustomDockNames; + QList> extraCustomDocks; + +#ifdef BROWSER_AVAILABLE + QPointer extraBrowserMenuDocksSeparator; + + QList> extraBrowserDocks; + QStringList extraBrowserDockNames; + QStringList extraBrowserDockTargets; + + void ClearExtraBrowserDocks(); + void LoadExtraBrowserDocks(); + void SaveExtraBrowserDocks(); + void ManageExtraBrowserDocks(); + void AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate); +#endif + + QIcon imageIcon; + QIcon colorIcon; + QIcon slideshowIcon; + QIcon audioInputIcon; + QIcon audioOutputIcon; + QIcon desktopCapIcon; + QIcon windowCapIcon; + QIcon gameCapIcon; + QIcon cameraIcon; + QIcon textIcon; + QIcon mediaIcon; + QIcon browserIcon; + QIcon groupIcon; + QIcon sceneIcon; + QIcon defaultIcon; + QIcon audioProcessOutputIcon; + + QIcon GetImageIcon() const; + QIcon GetColorIcon() const; + QIcon GetSlideshowIcon() const; + QIcon GetAudioInputIcon() const; + QIcon GetAudioOutputIcon() const; + QIcon GetDesktopCapIcon() const; + QIcon GetWindowCapIcon() const; + QIcon GetGameCapIcon() const; + QIcon GetCameraIcon() const; + QIcon GetTextIcon() const; + QIcon GetMediaIcon() const; + QIcon GetBrowserIcon() const; + QIcon GetDefaultIcon() const; + QIcon GetAudioProcessOutputIcon() const; + + QSlider *tBar; + bool tBarActive = false; + + OBSSource GetOverrideTransition(OBSSource source); + int GetOverrideTransitionDuration(OBSSource source); + + void UpdateProjectorHideCursor(); + void UpdateProjectorAlwaysOnTop(bool top); + void ResetProjectors(); + + QPointer screenshotData; + + void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); + + bool autoStartBroadcast = true; + bool autoStopBroadcast = true; + bool broadcastActive = false; + bool broadcastReady = false; + QPointer youtubeStreamCheckThread; +#ifdef YOUTUBE_ENABLED + void YoutubeStreamCheck(const std::string &key); + void ShowYouTubeAutoStartWarning(); + void YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now); +#endif + void BroadcastButtonClicked(); + void SetBroadcastFlowEnabled(bool enabled); + + void UpdatePreviewSafeAreas(); + bool drawSafeAreas = false; + + void CenterSelectedSceneItems(const CenterType ¢erType); + void ShowMissingFilesDialog(obs_missing_files_t *files); + + QColor selectionColor; + QColor cropColor; + QColor hoverColor; + + QColor GetCropColor() const; + QColor GetHoverColor() const; + + void UpdatePreviewSpacingHelpers(); + bool drawSpacingHelpers = true; + + float GetDevicePixelRatio(); + void SourceToolBarActionsSetEnabled(); + + std::string lastScreenshot; + std::string lastReplay; + + void UpdatePreviewOverflowSettings(); + void UpdatePreviewScrollbars(); + + bool streamingStarting = false; + + bool recordingStarted = false; + bool isRecordingPausable = false; + bool recordingPaused = false; + + bool restartingVCam = false; + +public slots: + void DeferSaveBegin(); + void DeferSaveEnd(); + + void DisplayStreamStartError(); + + void SetupBroadcast(); + + void StartStreaming(); + void StopStreaming(); + void ForceStopStreaming(); + + void StreamDelayStarting(int sec); + void StreamDelayStopping(int sec); + + void StreamingStart(); + void StreamStopping(); + void StreamingStop(int errorcode, QString last_error); + + void StartRecording(); + void StopRecording(); + + void RecordingStart(); + void RecordStopping(); + void RecordingStop(int code, QString last_error); + void RecordingFileChanged(QString lastRecordingPath); + + void ShowReplayBufferPauseWarning(); + void StartReplayBuffer(); + void StopReplayBuffer(); + + void ReplayBufferStart(); + void ReplayBufferSave(); + void ReplayBufferSaved(); + void ReplayBufferStopping(); + void ReplayBufferStop(int code); + + void StartVirtualCam(); + void StopVirtualCam(); + + void OnVirtualCamStart(); + void OnVirtualCamStop(int code); + + void SaveProjectDeferred(); + void SaveProject(); + + void SetTransition(OBSSource transition); + void OverrideTransition(OBSSource transition); + void TransitionToScene(OBSScene scene, bool force = false); + void TransitionToScene(OBSSource scene, bool force = false, bool quickTransition = false, int quickDuration = 0, + bool black = false, bool manual = false); + void SetCurrentScene(OBSSource scene, bool force = false); + + void UpdatePatronJson(const QString &text, const QString &error); + + void ShowContextBar(); + void HideContextBar(); + void PauseRecording(); + void UnpauseRecording(); + + void UpdateEditMenu(); + +private slots: + + void on_actionMainUndo_triggered(); + void on_actionMainRedo_triggered(); + + void AddSceneItem(OBSSceneItem item); + void AddScene(OBSSource source); + void RemoveScene(OBSSource source); + void RenameSources(OBSSource source, QString newName, QString prevName); + + void ActivateAudioSource(OBSSource source); + void DeactivateAudioSource(OBSSource source); + + void DuplicateSelectedScene(); + void RemoveSelectedScene(); + + void ToggleAlwaysOnTop(); + + void ReorderSources(OBSScene scene); + void RefreshSources(OBSScene scene); + + void ProcessHotkey(obs_hotkey_id id, bool pressed); + + void AddTransition(const char *id); + void RenameTransition(OBSSource transition); + void TransitionClicked(); + void TransitionStopped(); + void TransitionFullyStopped(); + void TriggerQuickTransition(int id); + + void SetDeinterlacingMode(); + void SetDeinterlacingOrder(); + + void SetScaleFilter(); + + void SetBlendingMethod(); + void SetBlendingMode(); + + void IconActivated(QSystemTrayIcon::ActivationReason reason); + void SetShowing(bool showing); + + void ToggleShowHide(); + + void HideAudioControl(); + void UnhideAllAudioControls(); + void ToggleHideMixer(); + + void MixerRenameSource(); + + void on_vMixerScrollArea_customContextMenuRequested(); + void on_hMixerScrollArea_customContextMenuRequested(); + + void on_actionCopySource_triggered(); + void on_actionPasteRef_triggered(); + void on_actionPasteDup_triggered(); + + void on_actionCopyFilters_triggered(); + void on_actionPasteFilters_triggered(); + void AudioMixerCopyFilters(); + void AudioMixerPasteFilters(); + void SourcePasteFilters(OBSSource source, OBSSource dstSource); + + void on_previewXScrollBar_valueChanged(int value); + void on_previewYScrollBar_valueChanged(int value); + + void PreviewScalingModeChanged(int value); + + void ColorChange(); + + SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); + + void on_actionShowAbout_triggered(); + + void EnablePreview(); + void DisablePreview(); + + void EnablePreviewProgram(); + void DisablePreviewProgram(); + + void SceneCopyFilters(); + void ScenePasteFilters(); + + void CheckDiskSpaceRemaining(); + void OpenSavedProjector(SavedProjectorInfo *info); + + void ResetStatsHotkey(); + + void SetImageIcon(const QIcon &icon); + void SetColorIcon(const QIcon &icon); + void SetSlideshowIcon(const QIcon &icon); + void SetAudioInputIcon(const QIcon &icon); + void SetAudioOutputIcon(const QIcon &icon); + void SetDesktopCapIcon(const QIcon &icon); + void SetWindowCapIcon(const QIcon &icon); + void SetGameCapIcon(const QIcon &icon); + void SetCameraIcon(const QIcon &icon); + void SetTextIcon(const QIcon &icon); + void SetMediaIcon(const QIcon &icon); + void SetBrowserIcon(const QIcon &icon); + void SetGroupIcon(const QIcon &icon); + void SetSceneIcon(const QIcon &icon); + void SetDefaultIcon(const QIcon &icon); + void SetAudioProcessOutputIcon(const QIcon &icon); + + void TBarChanged(int value); + void TBarReleased(); + + void LockVolumeControl(bool lock); + + void UpdateVirtualCamConfig(const VCamConfig &config); + void RestartVirtualCam(const VCamConfig &config); + void RestartingVirtualCam(); + +private: + /* OBS Callbacks */ + static void SceneReordered(void *data, calldata_t *params); + static void SceneRefreshed(void *data, calldata_t *params); + static void SceneItemAdded(void *data, calldata_t *params); + static void SourceCreated(void *data, calldata_t *params); + static void SourceRemoved(void *data, calldata_t *params); + static void SourceActivated(void *data, calldata_t *params); + static void SourceDeactivated(void *data, calldata_t *params); + static void SourceAudioActivated(void *data, calldata_t *params); + static void SourceAudioDeactivated(void *data, calldata_t *params); + static void SourceRenamed(void *data, calldata_t *params); + static void RenderMain(void *data, uint32_t cx, uint32_t cy); + + void ResizePreview(uint32_t cx, uint32_t cy); + + void AddSource(const char *id); + QMenu *CreateAddSourcePopupMenu(); + void AddSourcePopupMenu(const QPoint &pos); + void copyActionsDynamicProperties(); + + static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); + + void AutoRemux(QString input, bool no_show = false); + + void UpdateIsRecordingPausable(); + + bool IsFFmpegOutputToURL() const; + bool OutputPathValid(); + void OutputPathInvalidMessage(); + + bool LowDiskSpace(); + void DiskSpaceMessage(); + + OBSSource prevFTBSource = nullptr; + + float dpi = 1.0; + +public: + OBSSource GetProgramSource(); + OBSScene GetCurrentScene(); + + void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); + + inline OBSSource GetCurrentSceneSource() + { + OBSScene curScene = GetCurrentScene(); + return OBSSource(obs_scene_get_source(curScene)); + } + + obs_service_t *GetService(); + void SetService(obs_service_t *service); + + int GetTransitionDuration(); + int GetTbarPosition(); + + inline bool IsPreviewProgramMode() const { return os_atomic_load_bool(&previewProgramMode); } + + inline bool VCamEnabled() const { return vcamEnabled; } + + bool Active() const; + + void ResetUI(); + int ResetVideo(); + bool ResetAudio(); + + void ResetOutputs(); + + void RefreshVolumeColors(); + + void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); + + void NewProject(); + void LoadProject(); + + inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) + { + x = previewX; + y = previewY; + cx = previewCX; + cy = previewCY; + } + + inline bool SavingDisabled() const { return disableSaving; } + + inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); } + + void SaveService(); + bool LoadService(); + + inline Auth *GetAuth() { return auth.get(); } + + inline void EnableOutputs(bool enable) + { + if (enable) { + if (--disableOutputsRef < 0) + disableOutputsRef = 0; + } else { + disableOutputsRef++; + } + } + + QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); + QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item); + void CreateSourcePopupMenu(int idx, bool preview); + + void UpdateTitleBar(); + + void SystemTrayInit(); + void SystemTray(bool firstStarted); + + void OpenSavedProjectors(); + + void CreateInteractionWindow(obs_source_t *source); + void CreatePropertiesWindow(obs_source_t *source); + void CreateFiltersWindow(obs_source_t *source); + void CreateEditTransformWindow(obs_sceneitem_t *item); + + QAction *AddDockWidget(QDockWidget *dock); + void AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser = false); + void RemoveDockWidget(const QString &name); + bool IsDockObjectNameUsed(const QString &name); + void AddCustomDockWidget(QDockWidget *dock); + + static OBSBasic *Get(); + + const char *GetCurrentOutputPath(); + + void DeleteProjector(OBSProjector *projector); + + static QList GetProjectorMenuMonitorsFormatted(); + template + static void AddProjectorMenuMonitors(QMenu *parent, Receiver *target, void (Receiver::*slot)(Args...)) + { + auto projectors = GetProjectorMenuMonitorsFormatted(); + for (int i = 0; i < projectors.size(); i++) { + QString str = projectors[i]; + QAction *action = parent->addAction(str, target, slot); + action->setProperty("monitor", i); + } + } + + QIcon GetSourceIcon(const char *id) const; + QIcon GetGroupIcon() const; + QIcon GetSceneIcon() const; + + OBSWeakSource copyFilter; + + void ShowStatusBarMessage(const QString &message); + + static OBSData BackupScene(obs_scene_t *scene, std::vector *sources = nullptr); + void CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data); + + static inline OBSData BackupScene(obs_source_t *scene_source, std::vector *sources = nullptr) + { + obs_scene_t *scene = obs_scene_from_source(scene_source); + return BackupScene(scene, sources); + } + + void CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array); + + void SetDisplayAffinity(QWindow *window); + + QColor GetSelectionColor() const; + inline bool Closing() { return closing; } + +protected: + virtual void closeEvent(QCloseEvent *event) override; + virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; + virtual void changeEvent(QEvent *event) override; + +private slots: + void on_actionFullscreenInterface_triggered(); + + void on_actionShow_Recordings_triggered(); + void on_actionRemux_triggered(); + void on_action_Settings_triggered(); + void on_actionShowMacPermissions_triggered(); + void on_actionShowMissingFiles_triggered(); + void on_actionAdvAudioProperties_triggered(); + void on_actionMixerToolbarAdvAudio_triggered(); + void on_actionMixerToolbarMenu_triggered(); + void on_actionShowLogs_triggered(); + void on_actionUploadCurrentLog_triggered(); + void on_actionUploadLastLog_triggered(); + void on_actionViewCurrentLog_triggered(); + void on_actionCheckForUpdates_triggered(); + void on_actionRepair_triggered(); + void on_actionShowWhatsNew_triggered(); + void on_actionRestartSafe_triggered(); + + void on_actionShowCrashLogs_triggered(); + void on_actionUploadLastCrashLog_triggered(); + + void on_actionEditTransform_triggered(); + void on_actionCopyTransform_triggered(); + void on_actionPasteTransform_triggered(); + void on_actionRotate90CW_triggered(); + void on_actionRotate90CCW_triggered(); + void on_actionRotate180_triggered(); + void on_actionFlipHorizontal_triggered(); + void on_actionFlipVertical_triggered(); + void on_actionFitToScreen_triggered(); + void on_actionStretchToScreen_triggered(); + void on_actionCenterToScreen_triggered(); + void on_actionVerticalCenter_triggered(); + void on_actionHorizontalCenter_triggered(); + void on_actionSceneFilters_triggered(); + + void on_OBSBasic_customContextMenuRequested(const QPoint &pos); + + void on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *prev); + void on_scenes_customContextMenuRequested(const QPoint &pos); + void GridActionClicked(); + void on_actionSceneListMode_triggered(); + void on_actionSceneGridMode_triggered(); + void on_actionAddScene_triggered(); + void on_actionRemoveScene_triggered(); + void on_actionSceneUp_triggered(); + void on_actionSceneDown_triggered(); + void on_sources_customContextMenuRequested(const QPoint &pos); + void on_scenes_itemDoubleClicked(QListWidgetItem *item); + void on_actionAddSource_triggered(); + void on_actionRemoveSource_triggered(); + void on_actionInteract_triggered(); + void on_actionSourceProperties_triggered(); + void on_actionSourceUp_triggered(); + void on_actionSourceDown_triggered(); + + void on_actionMoveUp_triggered(); + void on_actionMoveDown_triggered(); + void on_actionMoveToTop_triggered(); + void on_actionMoveToBottom_triggered(); + + void on_actionLockPreview_triggered(); + + void on_scalingMenu_aboutToShow(); + void on_actionScaleWindow_triggered(); + void on_actionScaleCanvas_triggered(); + void on_actionScaleOutput_triggered(); + + void Screenshot(OBSSource source_ = nullptr); + void ScreenshotSelectedSource(); + void ScreenshotProgram(); + void ScreenshotScene(); + + void on_actionHelpPortal_triggered(); + void on_actionWebsite_triggered(); + void on_actionDiscord_triggered(); + void on_actionReleaseNotes_triggered(); + + void on_preview_customContextMenuRequested(); + void ProgramViewContextMenuRequested(); + void on_previewDisabledWidget_customContextMenuRequested(); + + void on_actionShowSettingsFolder_triggered(); + void on_actionShowProfileFolder_triggered(); + + void on_actionAlwaysOnTop_triggered(); + + void on_toggleListboxToolbars_toggled(bool visible); + void on_toggleContextBar_toggled(bool visible); + void on_toggleStatusBar_toggled(bool visible); + void on_toggleSourceIcons_toggled(bool visible); + + void on_transitions_currentIndexChanged(int index); + void on_transitionAdd_clicked(); + void on_transitionRemove_clicked(); + void on_transitionProps_clicked(); + void on_transitionDuration_valueChanged(); + + void ShowTransitionProperties(); + void HideTransitionProperties(); + + // Source Context Buttons + void on_sourcePropertiesButton_clicked(); + void on_sourceFiltersButton_clicked(); + void on_sourceInteractButton_clicked(); + + void on_autoConfigure_triggered(); + void on_stats_triggered(); + + void on_resetUI_triggered(); + void on_resetDocks_triggered(bool force = false); + void on_lockDocks_toggled(bool lock); + void on_multiviewProjectorWindowed_triggered(); + void on_sideDocks_toggled(bool side); + + void logUploadFinished(const QString &text, const QString &error); + void crashUploadFinished(const QString &text, const QString &error); + void openLogDialog(const QString &text, const bool crash); + + void updateCheckFinished(); + + void MoveSceneToTop(); + void MoveSceneToBottom(); + + void EditSceneName(); + void EditSceneItemName(); + + void SceneNameEdited(QWidget *editor); + + void OpenSceneFilters(); + void OpenFilters(OBSSource source = nullptr); + void OpenProperties(OBSSource source = nullptr); + void OpenInteraction(OBSSource source = nullptr); + void OpenEditTransform(OBSSceneItem item = nullptr); + + void EnablePreviewDisplay(bool enable); + void TogglePreview(); + + void OpenStudioProgramProjector(); + void OpenPreviewProjector(); + void OpenSourceProjector(); + void OpenMultiviewProjector(); + void OpenSceneProjector(); + + void OpenStudioProgramWindow(); + void OpenPreviewWindow(); + void OpenSourceWindow(); + void OpenSceneWindow(); + + void StackedMixerAreaContextMenuRequested(); + + void ResizeOutputSizeOfSource(); + + void RepairOldExtraDockName(); + void RepairCustomExtraDockName(); + + /* Stream action (start/stop) slot */ + void StreamActionTriggered(); + + /* Record action (start/stop) slot */ + void RecordActionTriggered(); + + /* Record pause (pause/unpause) slot */ + void RecordPauseToggled(); + + /* Replay Buffer action (start/stop) slot */ + void ReplayBufferActionTriggered(); + + /* Virtual Cam action (start/stop) slots */ + void VirtualCamActionTriggered(); + + void OpenVirtualCamConfig(); + + /* Studio Mode toggle slot */ + void TogglePreviewProgramMode(); + +public slots: + void on_actionResetTransform_triggered(); + + bool StreamingActive(); + bool RecordingActive(); + bool ReplayBufferActive(); + bool VirtualCamActive(); + + void ClearContextBar(); + void UpdateContextBar(bool force = false); + void UpdateContextBarDeferred(bool force = false); + void UpdateContextBarVisibility(); + +signals: + /* Streaming signals */ + void StreamingPreparing(); + void StreamingStarting(bool broadcastAutoStart); + void StreamingStarted(bool withDelay = false); + void StreamingStopping(); + void StreamingStopped(bool withDelay = false); + + /* Broadcast Flow signals */ + void BroadcastFlowEnabled(bool enabled); + void BroadcastStreamReady(bool ready); + void BroadcastStreamActive(); + void BroadcastStreamStarted(bool autoStop); + + /* Recording signals */ + void RecordingStarted(bool pausable = false); + void RecordingPaused(); + void RecordingUnpaused(); + void RecordingStopping(); + void RecordingStopped(); + + /* Replay Buffer signals */ + void ReplayBufEnabled(bool enabled); + void ReplayBufStarted(); + void ReplayBufStopping(); + void ReplayBufStopped(); + + /* Virtual Camera signals */ + void VirtualCamEnabled(); + void VirtualCamStarted(); + void VirtualCamStopped(); + + /* Studio Mode signal */ + void PreviewProgramModeChanged(bool enabled); + void CanvasResized(uint32_t width, uint32_t height); + void OutputResized(uint32_t width, uint32_t height); + + /* Preview signals */ + void PreviewXScrollBarMoved(int value); + void PreviewYScrollBarMoved(int value); + +private: + std::unique_ptr ui; + + QPointer controlsDock; + +public: + /* `undo_s` needs to be declared after `ui` to prevent an uninitialized + * warning for `ui` while initializing `undo_s`. */ + undo_stack undo_s; + + explicit OBSBasic(QWidget *parent = 0); + virtual ~OBSBasic(); + + virtual void OBSInit() override; + + virtual config_t *Config() const override; + + virtual int GetProfilePath(char *path, size_t size, const char *file) const override; + + static void InitBrowserPanelSafeBlock(); +#ifdef YOUTUBE_ENABLED + void NewYouTubeAppDock(); + void DeleteYouTubeAppDock(); + YouTubeAppDock *GetYouTubeAppDock(); +#endif + // MARK: - Generic UI Helper Functions + OBSPromptResult PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback); + + // MARK: - OBS Profile Management +private: + OBSProfileCache profiles{}; + + void SetupNewProfile(const std::string &profileName, bool useWizard = false); + void SetupDuplicateProfile(const std::string &profileName); + void SetupRenameProfile(const std::string &profileName); + + const OBSProfile &CreateProfile(const std::string &profileName); + void RemoveProfile(OBSProfile profile); + + void ChangeProfile(); + + void RefreshProfileCache(); + + void RefreshProfiles(bool refreshCache = false); + + void ActivateProfile(const OBSProfile &profile, bool reset = false); + void UpdateProfileEncoders(); + std::vector GetRestartRequirements(const ConfigFile &config) const; + void ResetProfileData(); + void CheckForSimpleModeX264Fallback(); + +public: + inline const OBSProfileCache &GetProfileCache() const noexcept { return profiles; }; + + const OBSProfile &GetCurrentProfile() const; + + std::optional GetProfileByName(const std::string &profileName) const; + std::optional GetProfileByDirectoryName(const std::string &directoryName) const; + +private slots: + void on_actionNewProfile_triggered(); + void on_actionDupProfile_triggered(); + void on_actionRenameProfile_triggered(); + void on_actionRemoveProfile_triggered(bool skipConfirmation = false); + void on_actionImportProfile_triggered(); + void on_actionExportProfile_triggered(); + +public slots: + bool CreateNewProfile(const QString &name); + bool CreateDuplicateProfile(const QString &name); + void DeleteProfile(const QString &profileName); + + // MARK: - OBS Scene Collection Management +private: + OBSSceneCollectionCache collections{}; + + void SetupNewSceneCollection(const std::string &collectionName); + void SetupDuplicateSceneCollection(const std::string &collectionName); + void SetupRenameSceneCollection(const std::string &collectionName); + + const OBSSceneCollection &CreateSceneCollection(const std::string &collectionName); + void RemoveSceneCollection(OBSSceneCollection collection); + + bool CreateDuplicateSceneCollection(const QString &name); + void DeleteSceneCollection(const QString &name); + void ChangeSceneCollection(); + + void RefreshSceneCollectionCache(); + + void RefreshSceneCollections(bool refreshCache = false); + void ActivateSceneCollection(const OBSSceneCollection &collection); + +public: + inline const OBSSceneCollectionCache &GetSceneCollectionCache() const noexcept { return collections; }; + + const OBSSceneCollection &GetCurrentSceneCollection() const; + + std::optional GetSceneCollectionByName(const std::string &collectionName) const; + std::optional GetSceneCollectionByFileName(const std::string &fileName) const; + +private slots: + void on_actionNewSceneCollection_triggered(); + void on_actionDupSceneCollection_triggered(); + void on_actionRenameSceneCollection_triggered(); + void on_actionRemoveSceneCollection_triggered(bool skipConfirmation = false); + void on_actionImportSceneCollection_triggered(); + void on_actionExportSceneCollection_triggered(); + void on_actionRemigrateSceneCollection_triggered(); + +public slots: + bool CreateNewSceneCollection(const QString &name); +}; + +extern bool cef_js_avail; + +class SceneRenameDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + SceneRenameDelegate(QObject *parent); + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + +protected: + virtual bool eventFilter(QObject *editor, QEvent *event) override; +}; diff --git a/UI/window-basic-main-screenshot.cpp b/frontend/utility/ScreenshotObj.cpp similarity index 100% rename from UI/window-basic-main-screenshot.cpp rename to frontend/utility/ScreenshotObj.cpp diff --git a/frontend/utility/SimpleOutput.cpp b/frontend/utility/SimpleOutput.cpp new file mode 100644 index 000000000..30d203174 --- /dev/null +++ b/frontend/utility/SimpleOutput.cpp @@ -0,0 +1,2541 @@ +#include +#include +#include +#include +#include +#include +#include "audio-encoders.hpp" +#include "multitrack-video-error.hpp" +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam.hpp" + +using namespace std; + +extern bool EncoderAvailable(const char *encoder); + +volatile bool streaming_active = false; +volatile bool recording_active = false; +volatile bool recording_paused = false; +volatile bool replaybuf_active = false; +volatile bool virtualcam_active = false; + +#define RTMP_PROTOCOL "rtmp" +#define SRT_PROTOCOL "srt" +#define RIST_PROTOCOL "rist" + +static void OBSStreamStarting(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + return; + + output->delayActive = true; + QMetaObject::invokeMethod(output->main, "StreamDelayStarting", Q_ARG(int, sec)); +} + +static void OBSStreamStopping(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + QMetaObject::invokeMethod(output->main, "StreamStopping"); + else + QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); +} + +static void OBSStartStreaming(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->streamingActive = true; + os_atomic_set_bool(&streaming_active, true); + QMetaObject::invokeMethod(output->main, "StreamingStart"); +} + +static void OBSStopStreaming(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->streamingActive = false; + output->delayActive = false; + output->multitrackVideoActive = false; + os_atomic_set_bool(&streaming_active, false); + QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSStartRecording(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->recordingActive = true; + os_atomic_set_bool(&recording_active, true); + QMetaObject::invokeMethod(output->main, "RecordingStart"); +} + +static void OBSStopRecording(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->recordingActive = false; + os_atomic_set_bool(&recording_active, false); + os_atomic_set_bool(&recording_paused, false); + QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSRecordStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "RecordStopping"); +} + +static void OBSRecordFileChanged(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + const char *next_file = calldata_string(params, "next_file"); + + QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); + + QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); + + output->lastRecordingPath = next_file; +} + +static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->replayBufferActive = true; + os_atomic_set_bool(&replaybuf_active, true); + QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); +} + +static void OBSStopReplayBuffer(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->replayBufferActive = false; + os_atomic_set_bool(&replaybuf_active, false); + QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); +} + +static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); +} + +static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); +} + +static void OBSStartVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->virtualCamActive = true; + os_atomic_set_bool(&virtualcam_active, true); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); +} + +static void OBSStopVirtualCam(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->virtualCamActive = false; + os_atomic_set_bool(&virtualcam_active, false); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); +} + +static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->DestroyVirtualCamView(); +} + +/* ------------------------------------------------------------------------ */ + +struct StartMultitrackVideoStreamingGuard { + StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; + ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } + + std::shared_future GetFuture() const { return future; } + + static std::shared_future MakeReadyFuture() + { + StartMultitrackVideoStreamingGuard guard; + return guard.GetFuture(); + } + +private: + std::promise guard; + std::shared_future future; +}; + +/* ------------------------------------------------------------------------ */ + +static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +static bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + +static const char *GetStreamOutputType(const obs_service_t *service) +{ + const char *protocol = obs_service_get_protocol(service); + const char *output = nullptr; + + if (!protocol) { + blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); + return nullptr; + } + + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); + return nullptr; + } + + /* Check if the service has a preferred output type */ + output = obs_service_get_preferred_output_type(service); + if (output) { + if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) + return output; + + blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); + } + + /* Otherwise, prefer first-party output types */ + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + return "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { + return "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { + return "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + obs_enum_output_types_with_protocol(protocol, &output, return_first_id); + if (output) + return output; + + blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); + + return nullptr; +} + +/* ------------------------------------------------------------------------ */ + +inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) +{ + if (main->vcamEnabled) { + virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); + + signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); + startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); + stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); + deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); + } + + auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); + if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { + auto service = main_->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); + } + if (multitrack_enabled) + multitrackVideo = make_unique(); +} + +extern void log_vcam_changed(const VCamConfig &config, bool starting); + +bool BasicOutputHandler::StartVirtualCam() +{ + if (!main->vcamEnabled) + return false; + + bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; + + if (!virtualCamView && !typeIsProgram) + virtualCamView = obs_view_create(); + + UpdateVirtualCamOutputSource(); + + if (!virtualCamVideo) { + virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); + + if (!virtualCamVideo) + return false; + } + + obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); + if (!Active()) + SetupOutputs(); + + bool success = obs_output_start(virtualCam); + if (!success) { + QString errorReason; + + const char *error = obs_output_get_last_error(virtualCam); + if (error) { + errorReason = QT_UTF8(error); + } else { + errorReason = QTStr("Output.StartFailedGeneric"); + } + + QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); + + DestroyVirtualCamView(); + } + + log_vcam_changed(main->vcamConfig, true); + + return success; +} + +void BasicOutputHandler::StopVirtualCam() +{ + if (main->vcamEnabled) { + obs_output_stop(virtualCam); + } +} + +bool BasicOutputHandler::VirtualCamActive() const +{ + if (main->vcamEnabled) { + return obs_output_active(virtualCam); + } + return false; +} + +void BasicOutputHandler::UpdateVirtualCamOutputSource() +{ + if (!main->vcamEnabled || !virtualCamView) + return; + + OBSSourceAutoRelease source; + + switch (main->vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + DestroyVirtualCameraScene(); + return; + case VCamOutputType::PreviewOutput: { + DestroyVirtualCameraScene(); + OBSSource s = main->GetCurrentSceneSource(); + obs_source_get_ref(s); + source = s.Get(); + break; + } + case VCamOutputType::SceneOutput: + DestroyVirtualCameraScene(); + source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); + + if (!vCamSourceScene) + vCamSourceScene = obs_scene_create_private("vcam_source"); + source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); + + if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { + obs_sceneitem_remove(vCamSourceSceneItem); + vCamSourceSceneItem = nullptr; + } + + if (!vCamSourceSceneItem) { + vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); + + obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); + obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); + + const struct vec2 size = { + (float)obs_source_get_width(source), + (float)obs_source_get_height(source), + }; + obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); + } + break; + } + + OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); + if (source != current) + obs_view_set_source(virtualCamView, 0, source); +} + +void BasicOutputHandler::DestroyVirtualCamView() +{ + if (main->vcamConfig.type == VCamOutputType::ProgramView) { + virtualCamVideo = nullptr; + return; + } + + obs_view_remove(virtualCamView); + obs_view_set_source(virtualCamView, 0, nullptr); + virtualCamVideo = nullptr; + + obs_view_destroy(virtualCamView); + virtualCamView = nullptr; + + DestroyVirtualCameraScene(); +} + +void BasicOutputHandler::DestroyVirtualCameraScene() +{ + if (!vCamSourceScene) + return; + + obs_scene_release(vCamSourceScene); + vCamSourceScene = nullptr; + vCamSourceSceneItem = nullptr; +} + +/* ------------------------------------------------------------------------ */ + +struct SimpleOutput : BasicOutputHandler { + OBSEncoder audioStreaming; + OBSEncoder videoStreaming; + OBSEncoder audioRecording; + OBSEncoder audioArchive; + OBSEncoder videoRecording; + OBSEncoder audioTrack[MAX_AUDIO_MIXES]; + + string videoEncoder; + string videoQuality; + bool usingRecordingPreset = false; + bool recordingConfigured = false; + bool ffmpegOutput = false; + bool lowCPUx264 = false; + + SimpleOutput(OBSBasic *main_); + + int CalcCRF(int crf); + + void UpdateRecordingSettings_x264_crf(int crf); + void UpdateRecordingSettings_qsv11(int crf, bool av1); + void UpdateRecordingSettings_nvenc(int cqp); + void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); + void UpdateRecordingSettings_amd_cqp(int cqp); + void UpdateRecordingSettings_apple(int quality); +#ifdef ENABLE_HEVC + void UpdateRecordingSettings_apple_hevc(int quality); +#endif + void UpdateRecordingSettings(); + void UpdateRecordingAudioSettings(); + virtual void Update() override; + + void SetupOutputs() override; + int GetAudioBitrate() const; + + void LoadRecordingPreset_Lossy(const char *encoder); + void LoadRecordingPreset_Lossless(); + void LoadRecordingPreset(); + + void LoadStreamingPreset_Lossy(const char *encoder); + + void UpdateRecording(); + bool ConfigureRecording(bool useReplayBuffer); + + bool IsVodTrackEnabled(obs_service_t *service); + void SetupVodTrack(obs_service_t *service); + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; +}; + +void SimpleOutput::LoadRecordingPreset_Lossless() +{ + fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(simple output)"; + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "format_name", "avi"); + obs_data_set_string(settings, "video_encoder", "utvideo"); + obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); + + obs_output_update(fileOutput, settings); +} + +void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) +{ + videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); + if (!videoRecording) + throw "Failed to create video recording encoder (simple output)"; + obs_encoder_release(videoRecording); +} + +void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) +{ + videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); + if (!videoStreaming) + throw "Failed to create video streaming encoder (simple output)"; + obs_encoder_release(videoStreaming); +} + +/* mistakes have been made to lead us to this. */ +const char *get_simple_output_encoder(const char *encoder) +{ + if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + return "obs_qsv11_v2"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + return "obs_qsv11_av1"; + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + return "h264_texture_amf"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + return "h265_texture_amf"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + return "av1_texture_amf"; + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + return "obs_nvenc_av1_tex"; + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.avc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.hevc"; +#endif + } + + return "obs_x264"; +} + +void SimpleOutput::LoadRecordingPreset() +{ + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); + + videoEncoder = encoder; + videoQuality = quality; + ffmpegOutput = false; + + if (strcmp(quality, "Stream") == 0) { + videoRecording = videoStreaming; + audioRecording = audioStreaming; + usingRecordingPreset = false; + return; + + } else if (strcmp(quality, "Lossless") == 0) { + LoadRecordingPreset_Lossless(); + usingRecordingPreset = true; + ffmpegOutput = true; + return; + + } else { + lowCPUx264 = false; + + if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) + lowCPUx264 = true; + LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); + usingRecordingPreset = true; + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); + else + success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); + + if (!success) + throw "Failed to create audio recording encoder " + "(simple output)"; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[23]; + if (strcmp(audio_encoder, "opus") == 0) { + snprintf(name, sizeof name, "simple_opus_recording%d", i); + success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } else { + snprintf(name, sizeof name, "simple_aac_recording%d", i); + success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } + if (!success) + throw "Failed to create multi-track audio recording encoder " + "(simple output)"; + } + } +} + +#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" + +SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + + LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); + else + success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); + + if (!success) + throw "Failed to create audio streaming encoder (simple output)"; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + else + success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + + if (!success) + throw "Failed to create audio archive encoder (simple output)"; + + LoadRecordingPreset(); + + if (!ffmpegOutput) { + bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", + nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(simple output)"; + } + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); +} + +int SimpleOutput::GetAudioBitrate() const +{ + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); + + if (strcmp(audio_encoder, "opus") == 0) + return FindClosestAvailableSimpleOpusBitrate(bitrate); + + return FindClosestAvailableSimpleAACBitrate(bitrate); +} + +void SimpleOutput::Update() +{ + OBSDataAutoRelease videoSettings = obs_data_create(); + OBSDataAutoRelease audioSettings = obs_data_create(); + + int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); + int audioBitrate = GetAudioBitrate(); + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *encoder_id = obs_encoder_get_id(videoStreaming); + const char *presetType; + const char *preset; + + if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + presetType = "AMDPreset"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + presetType = "AMDPreset"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + presetType = "NVENCPreset2"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + presetType = "NVENCPreset2"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + presetType = "AMDAV1Preset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + presetType = "NVENCPreset2"; + + } else { + presetType = "Preset"; + } + + preset = config_get_string(main->Config(), "SimpleOutput", presetType); + + /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ + if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { + obs_data_set_string(videoSettings, "preset2", preset); + } else { + obs_data_set_string(videoSettings, "preset", preset); + } + + obs_data_set_string(videoSettings, "rate_control", "CBR"); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + + if (advanced) + obs_data_set_string(videoSettings, "x264opts", custom); + + obs_data_set_string(audioSettings, "rate_control", "CBR"); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + + obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); + + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + } + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, videoSettings); + obs_encoder_update(audioStreaming, audioSettings); + obs_encoder_update(audioArchive, audioSettings); +} + +void SimpleOutput::UpdateRecordingAudioSettings() +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", 192); + obs_data_set_string(settings, "rate_control", "CBR"); + + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv || strcmp(quality, "Stream") == 0) { + obs_encoder_update(audioRecording, settings); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_update(audioTrack[i], settings); + } + } + } +} + +#define CROSS_DIST_CUTOFF 2000.0 + +int SimpleOutput::CalcCRF(int crf) +{ + int cx = config_get_uint(main->Config(), "Video", "OutputCX"); + int cy = config_get_uint(main->Config(), "Video", "OutputCY"); + double fCX = double(cx); + double fCY = double(cy); + + if (lowCPUx264) + crf -= 2; + + double crossDist = sqrt(fCX * fCX + fCY * fCY); + double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; + crfResReduction = (1.0 - crfResReduction) * 10.0; + + return crf - int(crfResReduction); +} + +void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "crf", crf); + obs_data_set_bool(settings, "use_bufsize", true); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); + + obs_encoder_update(videoRecording, settings); +} + +static bool icq_available(obs_encoder_t *encoder) +{ + obs_properties_t *props = obs_encoder_properties(encoder); + obs_property_t *p = obs_properties_get(props, "rate_control"); + bool icq_found = false; + + size_t num = obs_property_list_item_count(p); + for (size_t i = 0; i < num; i++) { + const char *val = obs_property_list_item_string(p, i); + if (strcmp(val, "ICQ") == 0) { + icq_found = true; + break; + } + } + + obs_properties_destroy(props); + return icq_found; +} + +void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) +{ + bool icq = icq_available(videoRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "profile", "high"); + + if (icq && !av1) { + obs_data_set_string(settings, "rate_control", "ICQ"); + obs_data_set_int(settings, "icq_quality", crf); + } else { + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_int(settings, "cqp", crf); + } + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_apple(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} + +#ifdef ENABLE_HEVC +void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} +#endif + +void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", "quality"); + obs_data_set_int(settings, "cqp", cqp); + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings() +{ + bool ultra_hq = (videoQuality == "HQ"); + int crf = CalcCRF(ultra_hq ? 16 : 23); + + if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { + UpdateRecordingSettings_x264_crf(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV) { + UpdateRecordingSettings_qsv11(crf, false); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { + UpdateRecordingSettings_qsv11(crf, true); + + } else if (videoEncoder == SIMPLE_ENCODER_AMD) { + UpdateRecordingSettings_amd_cqp(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { + UpdateRecordingSettings_amd_cqp(crf); +#endif + + } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { + UpdateRecordingSettings_amd_cqp(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { + UpdateRecordingSettings_nvenc(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); +#endif + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { + /* These are magic numbers. 0 - 100, more is better. */ + UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { + UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); +#endif + } + UpdateRecordingAudioSettings(); +} + +inline void SimpleOutput::SetupOutputs() +{ + SimpleOutput::Update(); + obs_encoder_set_video(videoStreaming, obs_get_video()); + obs_encoder_set_audio(audioStreaming, obs_get_audio()); + obs_encoder_set_audio(audioArchive, obs_get_audio()); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (usingRecordingPreset) { + if (ffmpegOutput) { + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + } else { + obs_encoder_set_video(videoRecording, obs_get_video()); + if (flv) { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_set_audio(audioTrack[i], obs_get_audio()); + } + } + } + } + } else { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } +} + +const char *FindAudioEncoderFromCodec(const char *type) +{ + const char *alt_enc_id = nullptr; + size_t i = 0; + + while (obs_enum_encoder_types(i++, &alt_enc_id)) { + const char *codec = obs_get_encoder_codec(alt_enc_id); + if (strcmp(type, codec) == 0) { + return alt_enc_id; + } + } + + return nullptr; +} + +std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) +{ + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + auto audio_bitrate = GetAudioBitrate(); + auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", + OBSStreamStopping, this); + + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, + this); + stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); + obs_output_set_service(streamOutput, service); + return true; + }; + + return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, + [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +static inline bool ServiceSupportsVodTrack(const char *service); + +static void clear_archive_encoder(obs_output_t *output, const char *expected_name) +{ + obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); + bool clear = false; + + /* ensures that we don't remove twitch's soundtrack encoder */ + if (last) { + const char *name = obs_encoder_get_name(last); + clear = name && strcmp(name, expected_name) == 0; + obs_encoder_release(last); + } + + if (clear) + obs_output_set_audio_encoder(output, nullptr, 1); +} + +bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) +{ + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *name = obs_data_get_string(settings, "service"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) + return enableForCustomServer ? enable : false; + else + return advanced && enable && ServiceSupportsVodTrack(name); +} + +void SimpleOutput::SetupVodTrack(obs_service_t *service) +{ + if (IsVodTrackEnabled(service)) + obs_output_set_audio_encoder(streamOutput, audioArchive, 1); + else + clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); +} + +bool SimpleOutput::StartStreaming(obs_service_t *service) +{ + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_uint(main->Config(), "Output", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + if (!reconnect) + maxRetries = 0; + + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + + if (!multitrackVideo || !multitrackVideoActive) + SetupVodTrack(service); + + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +void SimpleOutput::UpdateRecording() +{ + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + int idx = 0; + int idx2 = 0; + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + + if (replayBufferActive || recordingActive) + return; + + if (usingRecordingPreset) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(streamOutput)) { + Update(); + } + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput) { + obs_output_set_video_encoder(fileOutput, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(fileOutput, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); + } + } + } + } + if (replayBuffer) { + obs_output_set_video_encoder(replayBuffer, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); + } + } + } + } + + recordingConfigured = true; +} + +bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) +{ + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); + int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + + bool is_fragmented = strncmp(format, "fragmented", 10) == 0; + bool is_lossless = videoQuality == "Lossless"; + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + if (updateReplayBuffer) { + f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(format); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); + } else { + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, + f.c_str(), ffmpegOutput); + obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); + if (ffmpegOutput) + obs_output_set_mixers(fileOutput, tracks); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented && !is_lossless) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + if (updateReplayBuffer) + obs_output_update(replayBuffer, settings); + else + obs_output_update(fileOutput, settings); + + return true; +} + +bool SimpleOutput::StartRecording() +{ + UpdateRecording(); + if (!ConfigureRecording(false)) + return false; + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool SimpleOutput::StartReplayBuffer() +{ + UpdateRecording(); + if (!ConfigureRecording(true)) + return false; + if (!obs_output_start(replayBuffer)) { + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); + return false; + } + + return true; +} + +void SimpleOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void SimpleOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void SimpleOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool SimpleOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool SimpleOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool SimpleOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +struct AdvancedOutput : BasicOutputHandler { + OBSEncoder streamAudioEnc; + OBSEncoder streamArchiveEnc; + OBSEncoder streamTrack[MAX_AUDIO_MIXES]; + OBSEncoder recordTrack[MAX_AUDIO_MIXES]; + OBSEncoder videoStreaming; + OBSEncoder videoRecording; + + bool ffmpegOutput; + bool ffmpegRecording; + bool useStreamEncoder; + bool useStreamAudioEncoder; + bool usesBitrate = false; + + AdvancedOutput(OBSBasic *main_); + + inline void UpdateStreamSettings(); + inline void UpdateRecordingSettings(); + inline void UpdateAudioSettings(); + virtual void Update() override; + + inline std::optional VodTrackMixerIdx(obs_service_t *service); + inline void SetupVodTrack(obs_service_t *service); + + inline void SetupStreaming(); + inline void SetupRecording(); + inline void SetupFFmpeg(); + void SetupOutputs() override; + int GetAudioBitrate(size_t i, const char *id) const; + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; + bool allowsMultiTrack(); +}; + +static OBSData GetDataFromJsonFile(const char *jsonFile) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); + + OBSDataAutoRelease data = nullptr; + + if (!jsonFilePath.empty()) { + BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); + + if (!!jsonData) { + data = obs_data_create_from_json(jsonData); + } + } + + if (!data) { + data = obs_data_create(); + } + + return data.Get(); +} + +static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) +{ + OBSData dataRet = obs_encoder_get_defaults(encoder); + obs_data_release(dataRet); + + if (!!settings) + obs_data_apply(dataRet, settings); + settings = std::move(dataRet); +} + +#define ADV_ARCHIVE_NAME "adv_archive_audio" + +#ifdef __APPLE__ +static void translate_macvth264_encoder(const char *&encoder) +{ + if (strcmp(encoder, "vt_h264_hw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; + } else if (strcmp(encoder, "vt_h264_sw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264"; + } +} +#endif + +AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); +#ifdef __APPLE__ + translate_macvth264_encoder(streamEncoder); + translate_macvth264_encoder(recordEncoder); +#endif + + ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); + useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; + useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; + + OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); + OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); + + if (ffmpegOutput) { + fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(advanced output)"; + } else { + bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, + nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(advanced output)"; + + if (!useStreamEncoder) { + videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", + recordEncSettings, nullptr); + if (!videoRecording) + throw "Failed to create recording video " + "encoder (advanced output)"; + obs_encoder_release(videoRecording); + } + } + + videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); + if (!videoStreaming) + throw "Failed to create streaming video encoder " + "(advanced output)"; + obs_encoder_release(videoStreaming); + + const char *rate_control = + obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); + if (!rate_control) + rate_control = ""; + usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || + astrcmpi(rate_control, "ABR") == 0; + + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[19]; + snprintf(name, sizeof(name), "adv_record_audio_%d", i); + + recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, + name, nullptr, i, nullptr); + + if (!recordTrack[i]) { + throw "Failed to create audio encoder " + "(advanced output)"; + } + + obs_encoder_release(recordTrack[i]); + + snprintf(name, sizeof(name), "adv_stream_audio_%d", i); + streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); + + if (!streamTrack[i]) { + throw "Failed to create streaming audio encoders " + "(advanced output)"; + } + + obs_encoder_release(streamTrack[i]); + } + + std::string id; + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + streamAudioEnc = + obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); + if (!streamAudioEnc) + throw "Failed to create streaming audio encoder " + "(advanced output)"; + obs_encoder_release(streamAudioEnc); + + id = ""; + int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; + streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); + if (!streamArchiveEnc) + throw "Failed to create archive audio encoder " + "(advanced output)"; + obs_encoder_release(streamArchiveEnc); + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); + recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, + this); +} + +void AdvancedOutput::UpdateStreamSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + + OBSData settings = GetDataFromJsonFile("streamEncoder.json"); + ApplyEncoderDefaults(settings, videoStreaming); + + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings, "bitrate"); + int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(settings, "bitrate", bitrate); + } + + int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) + obs_data_set_int(settings, "keyint_sec", keyint_sec); + } else { + blog(LOG_WARNING, "User is ignoring service settings."); + } + + if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) + obs_data_set_bool(settings, "lookahead", false); + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, settings); +} + +inline void AdvancedOutput::UpdateRecordingSettings() +{ + OBSData settings = GetDataFromJsonFile("recordEncoder.json"); + obs_encoder_update(videoRecording, settings); +} + +void AdvancedOutput::Update() +{ + UpdateStreamSettings(); + if (!useStreamEncoder && !ffmpegOutput) + UpdateRecordingSettings(); + UpdateAudioSettings(); +} + +static inline bool ServiceSupportsVodTrack(const char *service) +{ + static const char *vodTrackServices[] = {"Twitch"}; + + for (const char *vodTrackService : vodTrackServices) { + if (astrcmpi(vodTrackService, service) == 0) + return true; + } + + return false; +} + +inline bool AdvancedOutput::allowsMultiTrack() +{ + const char *protocol = nullptr; + obs_service_t *service_obj = main->GetService(); + protocol = obs_service_get_protocol(service_obj); + if (!protocol) + return false; + return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || + astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; +} + +inline void AdvancedOutput::SetupStreaming() +{ + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + bool is_multitrack_output = allowsMultiTrack(); + + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + obs_encoder_set_scaled_size(videoStreaming, cx, cy); + obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); + + const char *id = obs_service_get_id(main->GetService()); + if (strcmp(id, "rtmp_custom") == 0) { + OBSDataAutoRelease settings = obs_data_create(); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + obs_encoder_update(videoStreaming, settings); + } +} + +inline void AdvancedOutput::SetupRecording() +{ + const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); + const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); + int tracks; + + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); + + bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv) + tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); + else + tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); + + OBSDataAutoRelease settings = obs_data_create(); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + + /* Hack to allow recordings without any audio tracks selected. It is no + * longer possible to select such a configuration in settings, but legacy + * configurations might still have this configured and we don't want to + * just break them. */ + if (tracks == 0) + tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + + if (useStreamEncoder) { + obs_output_set_video_encoder(fileOutput, videoStreaming); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoStreaming); + } else { + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + obs_encoder_set_scaled_size(videoRecording, cx, cy); + obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); + obs_output_set_video_encoder(fileOutput, videoRecording); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoRecording); + } + + if (!flv) { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); + idx++; + } + } + } else if (flv && tracks != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); + + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + obs_data_set_string(settings, "path", path); + obs_output_update(fileOutput, settings); + if (replayBuffer) + obs_output_update(replayBuffer, settings); +} + +inline void AdvancedOutput::SetupFFmpeg() +{ + const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); + int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); + int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); + bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); + const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); + const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); + const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); + const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); + int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); + const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); + int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); + int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); + const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); + int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); + const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); + + OBSDataArrayAutoRelease audio_names = obs_data_array_create(); + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + + const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + OBSDataAutoRelease item = obs_data_create(); + obs_data_set_string(item, "name", audioName); + obs_data_array_push_back(audio_names, item); + } + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_array(settings, "audio_names", audio_names); + obs_data_set_string(settings, "url", url); + obs_data_set_string(settings, "format_name", formatName); + obs_data_set_string(settings, "format_mime_type", mimeType); + obs_data_set_string(settings, "muxer_settings", muxCustom); + obs_data_set_int(settings, "gop_size", gopSize); + obs_data_set_int(settings, "video_bitrate", vBitrate); + obs_data_set_string(settings, "video_encoder", vEncoder); + obs_data_set_int(settings, "video_encoder_id", vEncoderId); + obs_data_set_string(settings, "video_settings", vEncCustom); + obs_data_set_int(settings, "audio_bitrate", aBitrate); + obs_data_set_string(settings, "audio_encoder", aEncoder); + obs_data_set_int(settings, "audio_encoder_id", aEncoderId); + obs_data_set_string(settings, "audio_settings", aEncCustom); + + if (rescale && rescaleRes && *rescaleRes) { + int width; + int height; + int val = sscanf(rescaleRes, "%dx%d", &width, &height); + + if (val == 2 && width && height) { + obs_data_set_int(settings, "scale_width", width); + obs_data_set_int(settings, "scale_height", height); + } + } + + obs_output_set_mixers(fileOutput, aMixes); + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + obs_output_update(fileOutput, settings); +} + +static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) +{ + obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); +} + +inline void AdvancedOutput::UpdateAudioSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + + bool is_multitrack_output = allowsMultiTrack(); + + OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + string def_name = "Track"; + def_name += to_string((int)i + 1); + SetEncoderName(recordTrack[i], name, def_name.c_str()); + SetEncoderName(streamTrack[i], name, def_name.c_str()); + } + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + int track = (int)(i + 1); + settings[i] = obs_data_create(); + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); + + obs_encoder_update(recordTrack[i], settings[i]); + + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); + + if (!is_multitrack_output) { + if (track == streamTrackIndex || track == vodTrackIndex) { + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); + obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); + + if (!enforceBitrate) + obs_data_set_int(settings[i], "bitrate", bitrate); + } + } + + if (track == streamTrackIndex) + obs_encoder_update(streamAudioEnc, settings[i]); + if (track == vodTrackIndex) + obs_encoder_update(streamArchiveEnc, settings[i]); + } else { + obs_encoder_update(streamTrack[i], settings[i]); + } + } +} + +void AdvancedOutput::SetupOutputs() +{ + obs_encoder_set_video(videoStreaming, obs_get_video()); + if (videoRecording) + obs_encoder_set_video(videoRecording, obs_get_video()); + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + obs_encoder_set_audio(streamTrack[i], obs_get_audio()); + obs_encoder_set_audio(recordTrack[i], obs_get_audio()); + } + obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); + obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); + + SetupStreaming(); + + if (ffmpegOutput) + SetupFFmpeg(); + else + SetupRecording(); +} + +int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const +{ + static const char *names[] = { + "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", + }; + int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); + return FindClosestAvailableAudioBitrate(id, bitrate); +} + +inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) +{ + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) { + vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; + } else { + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *service = obs_data_get_string(settings, "service"); + if (!ServiceSupportsVodTrack(service)) + vodTrackEnabled = false; + } + + if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) + return {vodTrackIndex - 1}; + return std::nullopt; +} + +inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +{ + if (VodTrackMixerIdx(service).has_value()) + obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); + else + clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); +} + +std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) +{ + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + + bool is_multitrack_output = allowsMultiTrack(); + + if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", + OBSStreamStopping, this); + + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, + this); + stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + int idx = 0; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + return true; + }; + + return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), + VodTrackMixerIdx(service), [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +bool AdvancedOutput::StartStreaming(obs_service_t *service) +{ + obs_output_set_service(streamOutput, service); + + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_int(main->Config(), "Output", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + bool is_rtmp = false; + obs_service_t *service_obj = main->GetService(); + const char *protocol = obs_service_get_protocol(service_obj); + if (protocol) { + if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) + is_rtmp = true; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + if (!reconnect) + maxRetries = 0; + + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + if (is_rtmp) { + SetupVodTrack(service); + } + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +bool AdvancedOutput::StartRecording() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + bool splitFile; + const char *splitFileType; + int splitFileTime; + int splitFileSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) { + UpdateRecordingSettings(); + } + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); + + string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, + ffmpegRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); + + if (splitFile) { + splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); + splitFileTime = (astrcmpi(splitFileType, "Time") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") + : 0; + splitFileSize = (astrcmpi(splitFileType, "Size") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") + : 0; + string ext = GetFormatExt(recFormat); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", filenameFormat); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); + obs_data_set_bool(settings, "split_file", true); + obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); + obs_data_set_int(settings, "max_size_mb", splitFileSize); + } + + obs_output_update(fileOutput, settings); + } + + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool AdvancedOutput::StartReplayBuffer() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + const char *rbPrefix; + const char *rbSuffix; + int rbTime; + int rbSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); + rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + + string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(recFormat); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); + + obs_output_update(replayBuffer, settings); + } + + if (!obs_output_start(replayBuffer)) { + QString error_reason; + const char *error = obs_output_get_last_error(replayBuffer); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); + return false; + } + + return true; +} + +void AdvancedOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void AdvancedOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void AdvancedOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool AdvancedOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool AdvancedOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool AdvancedOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +void BasicOutputHandler::SetupAutoRemux(const char *&container) +{ + bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); + if (autoRemux && strcmp(container, "mp4") == 0) + container = "mkv"; +} + +std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, + bool overwrite, const char *format, bool ffmpeg) +{ + if (!ffmpeg) + SetupAutoRemux(container); + + string dst = GetOutputFilename(path, container, noSpace, overwrite, format); + lastRecordingPath = dst; + return dst; +} + +extern std::string DeserializeConfigText(const char *text); + +std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, + size_t main_audio_mixer, + std::optional vod_track_mixer, + std::function)> continuation) +{ + auto start_streaming_guard = std::make_shared(); + if (!multitrackVideo) { + continuation(std::nullopt); + return start_streaming_guard->GetFuture(); + } + + multitrackVideoActive = false; + + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; + + std::optional custom_config = std::nullopt; + if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) + custom_config = DeserializeConfigText( + config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + QString key = obs_data_get_string(settings, "key"); + + const char *service_name = ""; + if (is_custom && obs_data_has_user_value(settings, "service_name")) { + service_name = obs_data_get_string(settings, "service_name"); + } else if (!is_custom) { + service_name = obs_data_get_string(settings, "service"); + } + + std::optional custom_rtmp_url; + std::optional use_rtmps; + auto server = obs_data_get_string(settings, "server"); + if (strncmp(server, "auto", 4) != 0) { + custom_rtmp_url = server; + } else { + QString server_ = server; + use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); + } + + auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); + if (custom_rtmp_url.has_value()) { + blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); + } + + auto maximum_aggregate_bitrate = + config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") + ? std::nullopt + : std::make_optional( + config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); + + auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") + ? std::nullopt + : std::make_optional(config_get_int( + main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); + + auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); + + auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, + continuation = + std::move(continuation)](std::optional error) { + if (error) { + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + multitrackVideoActive = false; + if (!error->ShowDialog(main, multitrack_video_name)) + return continuation(false); + return continuation(std::nullopt); + } + + multitrackVideoActive = true; + + auto signal_handler = multitrackVideo->StreamingSignalHandler(); + + streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); + streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); + + startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); + stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); + return continuation(true); + }; + + QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), + service_name = std::string{service_name}, service = OBSService{service}, + stream_dump_config = OBSData{stream_dump_config}, + start_streaming_guard = start_streaming_guard]() mutable { + std::optional error; + try { + multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, + audio_encoder_id.c_str(), maximum_aggregate_bitrate, + maximum_video_tracks, custom_config, stream_dump_config, + main_audio_mixer, vod_track_mixer, use_rtmps); + } catch (const MultitrackVideoError &error_) { + error.emplace(error_); + } + + QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); + }); + + return start_streaming_guard->GetFuture(); +} + +OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() +{ + auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); + + if (!stream_dump_enabled) + return nullptr; + + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), + // never remux stream dump + false); + obs_data_set_string(settings, "path", strPath.c_str()); + + if (useMP4) { + obs_data_set_bool(settings, "use_mp4", true); + obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); + } + + return settings; +} + +BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) +{ + return new SimpleOutput(main); +} + +BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) +{ + return new AdvancedOutput(main); +} diff --git a/frontend/utility/SimpleOutput.hpp b/frontend/utility/SimpleOutput.hpp new file mode 100644 index 000000000..30d203174 --- /dev/null +++ b/frontend/utility/SimpleOutput.hpp @@ -0,0 +1,2541 @@ +#include +#include +#include +#include +#include +#include +#include "audio-encoders.hpp" +#include "multitrack-video-error.hpp" +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam.hpp" + +using namespace std; + +extern bool EncoderAvailable(const char *encoder); + +volatile bool streaming_active = false; +volatile bool recording_active = false; +volatile bool recording_paused = false; +volatile bool replaybuf_active = false; +volatile bool virtualcam_active = false; + +#define RTMP_PROTOCOL "rtmp" +#define SRT_PROTOCOL "srt" +#define RIST_PROTOCOL "rist" + +static void OBSStreamStarting(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + return; + + output->delayActive = true; + QMetaObject::invokeMethod(output->main, "StreamDelayStarting", Q_ARG(int, sec)); +} + +static void OBSStreamStopping(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + QMetaObject::invokeMethod(output->main, "StreamStopping"); + else + QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); +} + +static void OBSStartStreaming(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->streamingActive = true; + os_atomic_set_bool(&streaming_active, true); + QMetaObject::invokeMethod(output->main, "StreamingStart"); +} + +static void OBSStopStreaming(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->streamingActive = false; + output->delayActive = false; + output->multitrackVideoActive = false; + os_atomic_set_bool(&streaming_active, false); + QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSStartRecording(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->recordingActive = true; + os_atomic_set_bool(&recording_active, true); + QMetaObject::invokeMethod(output->main, "RecordingStart"); +} + +static void OBSStopRecording(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->recordingActive = false; + os_atomic_set_bool(&recording_active, false); + os_atomic_set_bool(&recording_paused, false); + QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSRecordStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "RecordStopping"); +} + +static void OBSRecordFileChanged(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + const char *next_file = calldata_string(params, "next_file"); + + QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); + + QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); + + output->lastRecordingPath = next_file; +} + +static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->replayBufferActive = true; + os_atomic_set_bool(&replaybuf_active, true); + QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); +} + +static void OBSStopReplayBuffer(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->replayBufferActive = false; + os_atomic_set_bool(&replaybuf_active, false); + QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); +} + +static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); +} + +static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); +} + +static void OBSStartVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->virtualCamActive = true; + os_atomic_set_bool(&virtualcam_active, true); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); +} + +static void OBSStopVirtualCam(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->virtualCamActive = false; + os_atomic_set_bool(&virtualcam_active, false); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); +} + +static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->DestroyVirtualCamView(); +} + +/* ------------------------------------------------------------------------ */ + +struct StartMultitrackVideoStreamingGuard { + StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; + ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } + + std::shared_future GetFuture() const { return future; } + + static std::shared_future MakeReadyFuture() + { + StartMultitrackVideoStreamingGuard guard; + return guard.GetFuture(); + } + +private: + std::promise guard; + std::shared_future future; +}; + +/* ------------------------------------------------------------------------ */ + +static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +static bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + +static const char *GetStreamOutputType(const obs_service_t *service) +{ + const char *protocol = obs_service_get_protocol(service); + const char *output = nullptr; + + if (!protocol) { + blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); + return nullptr; + } + + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); + return nullptr; + } + + /* Check if the service has a preferred output type */ + output = obs_service_get_preferred_output_type(service); + if (output) { + if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) + return output; + + blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); + } + + /* Otherwise, prefer first-party output types */ + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + return "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { + return "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { + return "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + obs_enum_output_types_with_protocol(protocol, &output, return_first_id); + if (output) + return output; + + blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); + + return nullptr; +} + +/* ------------------------------------------------------------------------ */ + +inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) +{ + if (main->vcamEnabled) { + virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); + + signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); + startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); + stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); + deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); + } + + auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); + if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { + auto service = main_->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); + } + if (multitrack_enabled) + multitrackVideo = make_unique(); +} + +extern void log_vcam_changed(const VCamConfig &config, bool starting); + +bool BasicOutputHandler::StartVirtualCam() +{ + if (!main->vcamEnabled) + return false; + + bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; + + if (!virtualCamView && !typeIsProgram) + virtualCamView = obs_view_create(); + + UpdateVirtualCamOutputSource(); + + if (!virtualCamVideo) { + virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); + + if (!virtualCamVideo) + return false; + } + + obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); + if (!Active()) + SetupOutputs(); + + bool success = obs_output_start(virtualCam); + if (!success) { + QString errorReason; + + const char *error = obs_output_get_last_error(virtualCam); + if (error) { + errorReason = QT_UTF8(error); + } else { + errorReason = QTStr("Output.StartFailedGeneric"); + } + + QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); + + DestroyVirtualCamView(); + } + + log_vcam_changed(main->vcamConfig, true); + + return success; +} + +void BasicOutputHandler::StopVirtualCam() +{ + if (main->vcamEnabled) { + obs_output_stop(virtualCam); + } +} + +bool BasicOutputHandler::VirtualCamActive() const +{ + if (main->vcamEnabled) { + return obs_output_active(virtualCam); + } + return false; +} + +void BasicOutputHandler::UpdateVirtualCamOutputSource() +{ + if (!main->vcamEnabled || !virtualCamView) + return; + + OBSSourceAutoRelease source; + + switch (main->vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + DestroyVirtualCameraScene(); + return; + case VCamOutputType::PreviewOutput: { + DestroyVirtualCameraScene(); + OBSSource s = main->GetCurrentSceneSource(); + obs_source_get_ref(s); + source = s.Get(); + break; + } + case VCamOutputType::SceneOutput: + DestroyVirtualCameraScene(); + source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); + + if (!vCamSourceScene) + vCamSourceScene = obs_scene_create_private("vcam_source"); + source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); + + if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { + obs_sceneitem_remove(vCamSourceSceneItem); + vCamSourceSceneItem = nullptr; + } + + if (!vCamSourceSceneItem) { + vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); + + obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); + obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); + + const struct vec2 size = { + (float)obs_source_get_width(source), + (float)obs_source_get_height(source), + }; + obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); + } + break; + } + + OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); + if (source != current) + obs_view_set_source(virtualCamView, 0, source); +} + +void BasicOutputHandler::DestroyVirtualCamView() +{ + if (main->vcamConfig.type == VCamOutputType::ProgramView) { + virtualCamVideo = nullptr; + return; + } + + obs_view_remove(virtualCamView); + obs_view_set_source(virtualCamView, 0, nullptr); + virtualCamVideo = nullptr; + + obs_view_destroy(virtualCamView); + virtualCamView = nullptr; + + DestroyVirtualCameraScene(); +} + +void BasicOutputHandler::DestroyVirtualCameraScene() +{ + if (!vCamSourceScene) + return; + + obs_scene_release(vCamSourceScene); + vCamSourceScene = nullptr; + vCamSourceSceneItem = nullptr; +} + +/* ------------------------------------------------------------------------ */ + +struct SimpleOutput : BasicOutputHandler { + OBSEncoder audioStreaming; + OBSEncoder videoStreaming; + OBSEncoder audioRecording; + OBSEncoder audioArchive; + OBSEncoder videoRecording; + OBSEncoder audioTrack[MAX_AUDIO_MIXES]; + + string videoEncoder; + string videoQuality; + bool usingRecordingPreset = false; + bool recordingConfigured = false; + bool ffmpegOutput = false; + bool lowCPUx264 = false; + + SimpleOutput(OBSBasic *main_); + + int CalcCRF(int crf); + + void UpdateRecordingSettings_x264_crf(int crf); + void UpdateRecordingSettings_qsv11(int crf, bool av1); + void UpdateRecordingSettings_nvenc(int cqp); + void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); + void UpdateRecordingSettings_amd_cqp(int cqp); + void UpdateRecordingSettings_apple(int quality); +#ifdef ENABLE_HEVC + void UpdateRecordingSettings_apple_hevc(int quality); +#endif + void UpdateRecordingSettings(); + void UpdateRecordingAudioSettings(); + virtual void Update() override; + + void SetupOutputs() override; + int GetAudioBitrate() const; + + void LoadRecordingPreset_Lossy(const char *encoder); + void LoadRecordingPreset_Lossless(); + void LoadRecordingPreset(); + + void LoadStreamingPreset_Lossy(const char *encoder); + + void UpdateRecording(); + bool ConfigureRecording(bool useReplayBuffer); + + bool IsVodTrackEnabled(obs_service_t *service); + void SetupVodTrack(obs_service_t *service); + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; +}; + +void SimpleOutput::LoadRecordingPreset_Lossless() +{ + fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(simple output)"; + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "format_name", "avi"); + obs_data_set_string(settings, "video_encoder", "utvideo"); + obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); + + obs_output_update(fileOutput, settings); +} + +void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) +{ + videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); + if (!videoRecording) + throw "Failed to create video recording encoder (simple output)"; + obs_encoder_release(videoRecording); +} + +void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) +{ + videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); + if (!videoStreaming) + throw "Failed to create video streaming encoder (simple output)"; + obs_encoder_release(videoStreaming); +} + +/* mistakes have been made to lead us to this. */ +const char *get_simple_output_encoder(const char *encoder) +{ + if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + return "obs_qsv11_v2"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + return "obs_qsv11_av1"; + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + return "h264_texture_amf"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + return "h265_texture_amf"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + return "av1_texture_amf"; + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + return "obs_nvenc_av1_tex"; + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.avc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.hevc"; +#endif + } + + return "obs_x264"; +} + +void SimpleOutput::LoadRecordingPreset() +{ + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); + + videoEncoder = encoder; + videoQuality = quality; + ffmpegOutput = false; + + if (strcmp(quality, "Stream") == 0) { + videoRecording = videoStreaming; + audioRecording = audioStreaming; + usingRecordingPreset = false; + return; + + } else if (strcmp(quality, "Lossless") == 0) { + LoadRecordingPreset_Lossless(); + usingRecordingPreset = true; + ffmpegOutput = true; + return; + + } else { + lowCPUx264 = false; + + if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) + lowCPUx264 = true; + LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); + usingRecordingPreset = true; + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); + else + success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); + + if (!success) + throw "Failed to create audio recording encoder " + "(simple output)"; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[23]; + if (strcmp(audio_encoder, "opus") == 0) { + snprintf(name, sizeof name, "simple_opus_recording%d", i); + success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } else { + snprintf(name, sizeof name, "simple_aac_recording%d", i); + success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } + if (!success) + throw "Failed to create multi-track audio recording encoder " + "(simple output)"; + } + } +} + +#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" + +SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + + LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); + else + success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); + + if (!success) + throw "Failed to create audio streaming encoder (simple output)"; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + else + success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + + if (!success) + throw "Failed to create audio archive encoder (simple output)"; + + LoadRecordingPreset(); + + if (!ffmpegOutput) { + bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", + nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(simple output)"; + } + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); +} + +int SimpleOutput::GetAudioBitrate() const +{ + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); + + if (strcmp(audio_encoder, "opus") == 0) + return FindClosestAvailableSimpleOpusBitrate(bitrate); + + return FindClosestAvailableSimpleAACBitrate(bitrate); +} + +void SimpleOutput::Update() +{ + OBSDataAutoRelease videoSettings = obs_data_create(); + OBSDataAutoRelease audioSettings = obs_data_create(); + + int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); + int audioBitrate = GetAudioBitrate(); + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *encoder_id = obs_encoder_get_id(videoStreaming); + const char *presetType; + const char *preset; + + if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + presetType = "AMDPreset"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + presetType = "AMDPreset"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + presetType = "NVENCPreset2"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + presetType = "NVENCPreset2"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + presetType = "AMDAV1Preset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + presetType = "NVENCPreset2"; + + } else { + presetType = "Preset"; + } + + preset = config_get_string(main->Config(), "SimpleOutput", presetType); + + /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ + if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { + obs_data_set_string(videoSettings, "preset2", preset); + } else { + obs_data_set_string(videoSettings, "preset", preset); + } + + obs_data_set_string(videoSettings, "rate_control", "CBR"); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + + if (advanced) + obs_data_set_string(videoSettings, "x264opts", custom); + + obs_data_set_string(audioSettings, "rate_control", "CBR"); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + + obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); + + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + } + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, videoSettings); + obs_encoder_update(audioStreaming, audioSettings); + obs_encoder_update(audioArchive, audioSettings); +} + +void SimpleOutput::UpdateRecordingAudioSettings() +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", 192); + obs_data_set_string(settings, "rate_control", "CBR"); + + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv || strcmp(quality, "Stream") == 0) { + obs_encoder_update(audioRecording, settings); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_update(audioTrack[i], settings); + } + } + } +} + +#define CROSS_DIST_CUTOFF 2000.0 + +int SimpleOutput::CalcCRF(int crf) +{ + int cx = config_get_uint(main->Config(), "Video", "OutputCX"); + int cy = config_get_uint(main->Config(), "Video", "OutputCY"); + double fCX = double(cx); + double fCY = double(cy); + + if (lowCPUx264) + crf -= 2; + + double crossDist = sqrt(fCX * fCX + fCY * fCY); + double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; + crfResReduction = (1.0 - crfResReduction) * 10.0; + + return crf - int(crfResReduction); +} + +void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "crf", crf); + obs_data_set_bool(settings, "use_bufsize", true); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); + + obs_encoder_update(videoRecording, settings); +} + +static bool icq_available(obs_encoder_t *encoder) +{ + obs_properties_t *props = obs_encoder_properties(encoder); + obs_property_t *p = obs_properties_get(props, "rate_control"); + bool icq_found = false; + + size_t num = obs_property_list_item_count(p); + for (size_t i = 0; i < num; i++) { + const char *val = obs_property_list_item_string(p, i); + if (strcmp(val, "ICQ") == 0) { + icq_found = true; + break; + } + } + + obs_properties_destroy(props); + return icq_found; +} + +void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) +{ + bool icq = icq_available(videoRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "profile", "high"); + + if (icq && !av1) { + obs_data_set_string(settings, "rate_control", "ICQ"); + obs_data_set_int(settings, "icq_quality", crf); + } else { + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_int(settings, "cqp", crf); + } + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_apple(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} + +#ifdef ENABLE_HEVC +void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} +#endif + +void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", "quality"); + obs_data_set_int(settings, "cqp", cqp); + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings() +{ + bool ultra_hq = (videoQuality == "HQ"); + int crf = CalcCRF(ultra_hq ? 16 : 23); + + if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { + UpdateRecordingSettings_x264_crf(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV) { + UpdateRecordingSettings_qsv11(crf, false); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { + UpdateRecordingSettings_qsv11(crf, true); + + } else if (videoEncoder == SIMPLE_ENCODER_AMD) { + UpdateRecordingSettings_amd_cqp(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { + UpdateRecordingSettings_amd_cqp(crf); +#endif + + } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { + UpdateRecordingSettings_amd_cqp(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { + UpdateRecordingSettings_nvenc(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); +#endif + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { + /* These are magic numbers. 0 - 100, more is better. */ + UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { + UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); +#endif + } + UpdateRecordingAudioSettings(); +} + +inline void SimpleOutput::SetupOutputs() +{ + SimpleOutput::Update(); + obs_encoder_set_video(videoStreaming, obs_get_video()); + obs_encoder_set_audio(audioStreaming, obs_get_audio()); + obs_encoder_set_audio(audioArchive, obs_get_audio()); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (usingRecordingPreset) { + if (ffmpegOutput) { + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + } else { + obs_encoder_set_video(videoRecording, obs_get_video()); + if (flv) { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_set_audio(audioTrack[i], obs_get_audio()); + } + } + } + } + } else { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } +} + +const char *FindAudioEncoderFromCodec(const char *type) +{ + const char *alt_enc_id = nullptr; + size_t i = 0; + + while (obs_enum_encoder_types(i++, &alt_enc_id)) { + const char *codec = obs_get_encoder_codec(alt_enc_id); + if (strcmp(type, codec) == 0) { + return alt_enc_id; + } + } + + return nullptr; +} + +std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) +{ + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + auto audio_bitrate = GetAudioBitrate(); + auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", + OBSStreamStopping, this); + + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, + this); + stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); + obs_output_set_service(streamOutput, service); + return true; + }; + + return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, + [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +static inline bool ServiceSupportsVodTrack(const char *service); + +static void clear_archive_encoder(obs_output_t *output, const char *expected_name) +{ + obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); + bool clear = false; + + /* ensures that we don't remove twitch's soundtrack encoder */ + if (last) { + const char *name = obs_encoder_get_name(last); + clear = name && strcmp(name, expected_name) == 0; + obs_encoder_release(last); + } + + if (clear) + obs_output_set_audio_encoder(output, nullptr, 1); +} + +bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) +{ + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *name = obs_data_get_string(settings, "service"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) + return enableForCustomServer ? enable : false; + else + return advanced && enable && ServiceSupportsVodTrack(name); +} + +void SimpleOutput::SetupVodTrack(obs_service_t *service) +{ + if (IsVodTrackEnabled(service)) + obs_output_set_audio_encoder(streamOutput, audioArchive, 1); + else + clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); +} + +bool SimpleOutput::StartStreaming(obs_service_t *service) +{ + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_uint(main->Config(), "Output", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + if (!reconnect) + maxRetries = 0; + + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + + if (!multitrackVideo || !multitrackVideoActive) + SetupVodTrack(service); + + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +void SimpleOutput::UpdateRecording() +{ + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + int idx = 0; + int idx2 = 0; + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + + if (replayBufferActive || recordingActive) + return; + + if (usingRecordingPreset) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(streamOutput)) { + Update(); + } + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput) { + obs_output_set_video_encoder(fileOutput, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(fileOutput, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); + } + } + } + } + if (replayBuffer) { + obs_output_set_video_encoder(replayBuffer, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); + } + } + } + } + + recordingConfigured = true; +} + +bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) +{ + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); + int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + + bool is_fragmented = strncmp(format, "fragmented", 10) == 0; + bool is_lossless = videoQuality == "Lossless"; + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + if (updateReplayBuffer) { + f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(format); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); + } else { + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, + f.c_str(), ffmpegOutput); + obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); + if (ffmpegOutput) + obs_output_set_mixers(fileOutput, tracks); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented && !is_lossless) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + if (updateReplayBuffer) + obs_output_update(replayBuffer, settings); + else + obs_output_update(fileOutput, settings); + + return true; +} + +bool SimpleOutput::StartRecording() +{ + UpdateRecording(); + if (!ConfigureRecording(false)) + return false; + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool SimpleOutput::StartReplayBuffer() +{ + UpdateRecording(); + if (!ConfigureRecording(true)) + return false; + if (!obs_output_start(replayBuffer)) { + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); + return false; + } + + return true; +} + +void SimpleOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void SimpleOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void SimpleOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool SimpleOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool SimpleOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool SimpleOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +struct AdvancedOutput : BasicOutputHandler { + OBSEncoder streamAudioEnc; + OBSEncoder streamArchiveEnc; + OBSEncoder streamTrack[MAX_AUDIO_MIXES]; + OBSEncoder recordTrack[MAX_AUDIO_MIXES]; + OBSEncoder videoStreaming; + OBSEncoder videoRecording; + + bool ffmpegOutput; + bool ffmpegRecording; + bool useStreamEncoder; + bool useStreamAudioEncoder; + bool usesBitrate = false; + + AdvancedOutput(OBSBasic *main_); + + inline void UpdateStreamSettings(); + inline void UpdateRecordingSettings(); + inline void UpdateAudioSettings(); + virtual void Update() override; + + inline std::optional VodTrackMixerIdx(obs_service_t *service); + inline void SetupVodTrack(obs_service_t *service); + + inline void SetupStreaming(); + inline void SetupRecording(); + inline void SetupFFmpeg(); + void SetupOutputs() override; + int GetAudioBitrate(size_t i, const char *id) const; + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; + bool allowsMultiTrack(); +}; + +static OBSData GetDataFromJsonFile(const char *jsonFile) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); + + OBSDataAutoRelease data = nullptr; + + if (!jsonFilePath.empty()) { + BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); + + if (!!jsonData) { + data = obs_data_create_from_json(jsonData); + } + } + + if (!data) { + data = obs_data_create(); + } + + return data.Get(); +} + +static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) +{ + OBSData dataRet = obs_encoder_get_defaults(encoder); + obs_data_release(dataRet); + + if (!!settings) + obs_data_apply(dataRet, settings); + settings = std::move(dataRet); +} + +#define ADV_ARCHIVE_NAME "adv_archive_audio" + +#ifdef __APPLE__ +static void translate_macvth264_encoder(const char *&encoder) +{ + if (strcmp(encoder, "vt_h264_hw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; + } else if (strcmp(encoder, "vt_h264_sw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264"; + } +} +#endif + +AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); +#ifdef __APPLE__ + translate_macvth264_encoder(streamEncoder); + translate_macvth264_encoder(recordEncoder); +#endif + + ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); + useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; + useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; + + OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); + OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); + + if (ffmpegOutput) { + fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(advanced output)"; + } else { + bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, + nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(advanced output)"; + + if (!useStreamEncoder) { + videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", + recordEncSettings, nullptr); + if (!videoRecording) + throw "Failed to create recording video " + "encoder (advanced output)"; + obs_encoder_release(videoRecording); + } + } + + videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); + if (!videoStreaming) + throw "Failed to create streaming video encoder " + "(advanced output)"; + obs_encoder_release(videoStreaming); + + const char *rate_control = + obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); + if (!rate_control) + rate_control = ""; + usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || + astrcmpi(rate_control, "ABR") == 0; + + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[19]; + snprintf(name, sizeof(name), "adv_record_audio_%d", i); + + recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, + name, nullptr, i, nullptr); + + if (!recordTrack[i]) { + throw "Failed to create audio encoder " + "(advanced output)"; + } + + obs_encoder_release(recordTrack[i]); + + snprintf(name, sizeof(name), "adv_stream_audio_%d", i); + streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); + + if (!streamTrack[i]) { + throw "Failed to create streaming audio encoders " + "(advanced output)"; + } + + obs_encoder_release(streamTrack[i]); + } + + std::string id; + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + streamAudioEnc = + obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); + if (!streamAudioEnc) + throw "Failed to create streaming audio encoder " + "(advanced output)"; + obs_encoder_release(streamAudioEnc); + + id = ""; + int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; + streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); + if (!streamArchiveEnc) + throw "Failed to create archive audio encoder " + "(advanced output)"; + obs_encoder_release(streamArchiveEnc); + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); + recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, + this); +} + +void AdvancedOutput::UpdateStreamSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + + OBSData settings = GetDataFromJsonFile("streamEncoder.json"); + ApplyEncoderDefaults(settings, videoStreaming); + + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings, "bitrate"); + int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(settings, "bitrate", bitrate); + } + + int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) + obs_data_set_int(settings, "keyint_sec", keyint_sec); + } else { + blog(LOG_WARNING, "User is ignoring service settings."); + } + + if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) + obs_data_set_bool(settings, "lookahead", false); + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, settings); +} + +inline void AdvancedOutput::UpdateRecordingSettings() +{ + OBSData settings = GetDataFromJsonFile("recordEncoder.json"); + obs_encoder_update(videoRecording, settings); +} + +void AdvancedOutput::Update() +{ + UpdateStreamSettings(); + if (!useStreamEncoder && !ffmpegOutput) + UpdateRecordingSettings(); + UpdateAudioSettings(); +} + +static inline bool ServiceSupportsVodTrack(const char *service) +{ + static const char *vodTrackServices[] = {"Twitch"}; + + for (const char *vodTrackService : vodTrackServices) { + if (astrcmpi(vodTrackService, service) == 0) + return true; + } + + return false; +} + +inline bool AdvancedOutput::allowsMultiTrack() +{ + const char *protocol = nullptr; + obs_service_t *service_obj = main->GetService(); + protocol = obs_service_get_protocol(service_obj); + if (!protocol) + return false; + return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || + astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; +} + +inline void AdvancedOutput::SetupStreaming() +{ + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + bool is_multitrack_output = allowsMultiTrack(); + + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + obs_encoder_set_scaled_size(videoStreaming, cx, cy); + obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); + + const char *id = obs_service_get_id(main->GetService()); + if (strcmp(id, "rtmp_custom") == 0) { + OBSDataAutoRelease settings = obs_data_create(); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + obs_encoder_update(videoStreaming, settings); + } +} + +inline void AdvancedOutput::SetupRecording() +{ + const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); + const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); + int tracks; + + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); + + bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv) + tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); + else + tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); + + OBSDataAutoRelease settings = obs_data_create(); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + + /* Hack to allow recordings without any audio tracks selected. It is no + * longer possible to select such a configuration in settings, but legacy + * configurations might still have this configured and we don't want to + * just break them. */ + if (tracks == 0) + tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + + if (useStreamEncoder) { + obs_output_set_video_encoder(fileOutput, videoStreaming); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoStreaming); + } else { + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + obs_encoder_set_scaled_size(videoRecording, cx, cy); + obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); + obs_output_set_video_encoder(fileOutput, videoRecording); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoRecording); + } + + if (!flv) { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); + idx++; + } + } + } else if (flv && tracks != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); + + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + obs_data_set_string(settings, "path", path); + obs_output_update(fileOutput, settings); + if (replayBuffer) + obs_output_update(replayBuffer, settings); +} + +inline void AdvancedOutput::SetupFFmpeg() +{ + const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); + int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); + int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); + bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); + const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); + const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); + const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); + const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); + int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); + const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); + int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); + int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); + const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); + int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); + const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); + + OBSDataArrayAutoRelease audio_names = obs_data_array_create(); + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + + const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + OBSDataAutoRelease item = obs_data_create(); + obs_data_set_string(item, "name", audioName); + obs_data_array_push_back(audio_names, item); + } + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_array(settings, "audio_names", audio_names); + obs_data_set_string(settings, "url", url); + obs_data_set_string(settings, "format_name", formatName); + obs_data_set_string(settings, "format_mime_type", mimeType); + obs_data_set_string(settings, "muxer_settings", muxCustom); + obs_data_set_int(settings, "gop_size", gopSize); + obs_data_set_int(settings, "video_bitrate", vBitrate); + obs_data_set_string(settings, "video_encoder", vEncoder); + obs_data_set_int(settings, "video_encoder_id", vEncoderId); + obs_data_set_string(settings, "video_settings", vEncCustom); + obs_data_set_int(settings, "audio_bitrate", aBitrate); + obs_data_set_string(settings, "audio_encoder", aEncoder); + obs_data_set_int(settings, "audio_encoder_id", aEncoderId); + obs_data_set_string(settings, "audio_settings", aEncCustom); + + if (rescale && rescaleRes && *rescaleRes) { + int width; + int height; + int val = sscanf(rescaleRes, "%dx%d", &width, &height); + + if (val == 2 && width && height) { + obs_data_set_int(settings, "scale_width", width); + obs_data_set_int(settings, "scale_height", height); + } + } + + obs_output_set_mixers(fileOutput, aMixes); + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + obs_output_update(fileOutput, settings); +} + +static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) +{ + obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); +} + +inline void AdvancedOutput::UpdateAudioSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + + bool is_multitrack_output = allowsMultiTrack(); + + OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + string def_name = "Track"; + def_name += to_string((int)i + 1); + SetEncoderName(recordTrack[i], name, def_name.c_str()); + SetEncoderName(streamTrack[i], name, def_name.c_str()); + } + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + int track = (int)(i + 1); + settings[i] = obs_data_create(); + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); + + obs_encoder_update(recordTrack[i], settings[i]); + + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); + + if (!is_multitrack_output) { + if (track == streamTrackIndex || track == vodTrackIndex) { + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); + obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); + + if (!enforceBitrate) + obs_data_set_int(settings[i], "bitrate", bitrate); + } + } + + if (track == streamTrackIndex) + obs_encoder_update(streamAudioEnc, settings[i]); + if (track == vodTrackIndex) + obs_encoder_update(streamArchiveEnc, settings[i]); + } else { + obs_encoder_update(streamTrack[i], settings[i]); + } + } +} + +void AdvancedOutput::SetupOutputs() +{ + obs_encoder_set_video(videoStreaming, obs_get_video()); + if (videoRecording) + obs_encoder_set_video(videoRecording, obs_get_video()); + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + obs_encoder_set_audio(streamTrack[i], obs_get_audio()); + obs_encoder_set_audio(recordTrack[i], obs_get_audio()); + } + obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); + obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); + + SetupStreaming(); + + if (ffmpegOutput) + SetupFFmpeg(); + else + SetupRecording(); +} + +int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const +{ + static const char *names[] = { + "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", + }; + int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); + return FindClosestAvailableAudioBitrate(id, bitrate); +} + +inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) +{ + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) { + vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; + } else { + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *service = obs_data_get_string(settings, "service"); + if (!ServiceSupportsVodTrack(service)) + vodTrackEnabled = false; + } + + if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) + return {vodTrackIndex - 1}; + return std::nullopt; +} + +inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +{ + if (VodTrackMixerIdx(service).has_value()) + obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); + else + clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); +} + +std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) +{ + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + + bool is_multitrack_output = allowsMultiTrack(); + + if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", + OBSStreamStopping, this); + + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, + this); + stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + int idx = 0; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + return true; + }; + + return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), + VodTrackMixerIdx(service), [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +bool AdvancedOutput::StartStreaming(obs_service_t *service) +{ + obs_output_set_service(streamOutput, service); + + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_int(main->Config(), "Output", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + bool is_rtmp = false; + obs_service_t *service_obj = main->GetService(); + const char *protocol = obs_service_get_protocol(service_obj); + if (protocol) { + if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) + is_rtmp = true; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + if (!reconnect) + maxRetries = 0; + + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + if (is_rtmp) { + SetupVodTrack(service); + } + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +bool AdvancedOutput::StartRecording() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + bool splitFile; + const char *splitFileType; + int splitFileTime; + int splitFileSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) { + UpdateRecordingSettings(); + } + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); + + string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, + ffmpegRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); + + if (splitFile) { + splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); + splitFileTime = (astrcmpi(splitFileType, "Time") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") + : 0; + splitFileSize = (astrcmpi(splitFileType, "Size") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") + : 0; + string ext = GetFormatExt(recFormat); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", filenameFormat); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); + obs_data_set_bool(settings, "split_file", true); + obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); + obs_data_set_int(settings, "max_size_mb", splitFileSize); + } + + obs_output_update(fileOutput, settings); + } + + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool AdvancedOutput::StartReplayBuffer() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + const char *rbPrefix; + const char *rbSuffix; + int rbTime; + int rbSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); + rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + + string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(recFormat); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); + + obs_output_update(replayBuffer, settings); + } + + if (!obs_output_start(replayBuffer)) { + QString error_reason; + const char *error = obs_output_get_last_error(replayBuffer); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); + return false; + } + + return true; +} + +void AdvancedOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void AdvancedOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void AdvancedOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool AdvancedOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool AdvancedOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool AdvancedOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +void BasicOutputHandler::SetupAutoRemux(const char *&container) +{ + bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); + if (autoRemux && strcmp(container, "mp4") == 0) + container = "mkv"; +} + +std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, + bool overwrite, const char *format, bool ffmpeg) +{ + if (!ffmpeg) + SetupAutoRemux(container); + + string dst = GetOutputFilename(path, container, noSpace, overwrite, format); + lastRecordingPath = dst; + return dst; +} + +extern std::string DeserializeConfigText(const char *text); + +std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, + size_t main_audio_mixer, + std::optional vod_track_mixer, + std::function)> continuation) +{ + auto start_streaming_guard = std::make_shared(); + if (!multitrackVideo) { + continuation(std::nullopt); + return start_streaming_guard->GetFuture(); + } + + multitrackVideoActive = false; + + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; + + std::optional custom_config = std::nullopt; + if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) + custom_config = DeserializeConfigText( + config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + QString key = obs_data_get_string(settings, "key"); + + const char *service_name = ""; + if (is_custom && obs_data_has_user_value(settings, "service_name")) { + service_name = obs_data_get_string(settings, "service_name"); + } else if (!is_custom) { + service_name = obs_data_get_string(settings, "service"); + } + + std::optional custom_rtmp_url; + std::optional use_rtmps; + auto server = obs_data_get_string(settings, "server"); + if (strncmp(server, "auto", 4) != 0) { + custom_rtmp_url = server; + } else { + QString server_ = server; + use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); + } + + auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); + if (custom_rtmp_url.has_value()) { + blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); + } + + auto maximum_aggregate_bitrate = + config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") + ? std::nullopt + : std::make_optional( + config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); + + auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") + ? std::nullopt + : std::make_optional(config_get_int( + main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); + + auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); + + auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, + continuation = + std::move(continuation)](std::optional error) { + if (error) { + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + multitrackVideoActive = false; + if (!error->ShowDialog(main, multitrack_video_name)) + return continuation(false); + return continuation(std::nullopt); + } + + multitrackVideoActive = true; + + auto signal_handler = multitrackVideo->StreamingSignalHandler(); + + streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); + streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); + + startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); + stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); + return continuation(true); + }; + + QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), + service_name = std::string{service_name}, service = OBSService{service}, + stream_dump_config = OBSData{stream_dump_config}, + start_streaming_guard = start_streaming_guard]() mutable { + std::optional error; + try { + multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, + audio_encoder_id.c_str(), maximum_aggregate_bitrate, + maximum_video_tracks, custom_config, stream_dump_config, + main_audio_mixer, vod_track_mixer, use_rtmps); + } catch (const MultitrackVideoError &error_) { + error.emplace(error_); + } + + QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); + }); + + return start_streaming_guard->GetFuture(); +} + +OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() +{ + auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); + + if (!stream_dump_enabled) + return nullptr; + + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), + // never remux stream dump + false); + obs_data_set_string(settings, "path", strPath.c_str()); + + if (useMP4) { + obs_data_set_bool(settings, "use_mp4", true); + obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); + } + + return settings; +} + +BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) +{ + return new SimpleOutput(main); +} + +BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) +{ + return new AdvancedOutput(main); +} diff --git a/frontend/utility/StartMultiTrackVideoStreamingGuard.hpp b/frontend/utility/StartMultiTrackVideoStreamingGuard.hpp new file mode 100644 index 000000000..30d203174 --- /dev/null +++ b/frontend/utility/StartMultiTrackVideoStreamingGuard.hpp @@ -0,0 +1,2541 @@ +#include +#include +#include +#include +#include +#include +#include "audio-encoders.hpp" +#include "multitrack-video-error.hpp" +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam.hpp" + +using namespace std; + +extern bool EncoderAvailable(const char *encoder); + +volatile bool streaming_active = false; +volatile bool recording_active = false; +volatile bool recording_paused = false; +volatile bool replaybuf_active = false; +volatile bool virtualcam_active = false; + +#define RTMP_PROTOCOL "rtmp" +#define SRT_PROTOCOL "srt" +#define RIST_PROTOCOL "rist" + +static void OBSStreamStarting(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + return; + + output->delayActive = true; + QMetaObject::invokeMethod(output->main, "StreamDelayStarting", Q_ARG(int, sec)); +} + +static void OBSStreamStopping(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + obs_output_t *obj = (obs_output_t *)calldata_ptr(params, "output"); + + int sec = (int)obs_output_get_active_delay(obj); + if (sec == 0) + QMetaObject::invokeMethod(output->main, "StreamStopping"); + else + QMetaObject::invokeMethod(output->main, "StreamDelayStopping", Q_ARG(int, sec)); +} + +static void OBSStartStreaming(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->streamingActive = true; + os_atomic_set_bool(&streaming_active, true); + QMetaObject::invokeMethod(output->main, "StreamingStart"); +} + +static void OBSStopStreaming(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->streamingActive = false; + output->delayActive = false; + output->multitrackVideoActive = false; + os_atomic_set_bool(&streaming_active, false); + QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSStartRecording(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->recordingActive = true; + os_atomic_set_bool(&recording_active, true); + QMetaObject::invokeMethod(output->main, "RecordingStart"); +} + +static void OBSStopRecording(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + const char *last_error = calldata_string(params, "last_error"); + + QString arg_last_error = QString::fromUtf8(last_error); + + output->recordingActive = false; + os_atomic_set_bool(&recording_active, false); + os_atomic_set_bool(&recording_paused, false); + QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); +} + +static void OBSRecordStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "RecordStopping"); +} + +static void OBSRecordFileChanged(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + const char *next_file = calldata_string(params, "next_file"); + + QString arg_last_file = QString::fromUtf8(output->lastRecordingPath.c_str()); + + QMetaObject::invokeMethod(output->main, "RecordingFileChanged", Q_ARG(QString, arg_last_file)); + + output->lastRecordingPath = next_file; +} + +static void OBSStartReplayBuffer(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->replayBufferActive = true; + os_atomic_set_bool(&replaybuf_active, true); + QMetaObject::invokeMethod(output->main, "ReplayBufferStart"); +} + +static void OBSStopReplayBuffer(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->replayBufferActive = false; + os_atomic_set_bool(&replaybuf_active, false); + QMetaObject::invokeMethod(output->main, "ReplayBufferStop", Q_ARG(int, code)); +} + +static void OBSReplayBufferStopping(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferStopping"); +} + +static void OBSReplayBufferSaved(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + QMetaObject::invokeMethod(output->main, "ReplayBufferSaved", Qt::QueuedConnection); +} + +static void OBSStartVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + + output->virtualCamActive = true; + os_atomic_set_bool(&virtualcam_active, true); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStart"); +} + +static void OBSStopVirtualCam(void *data, calldata_t *params) +{ + BasicOutputHandler *output = static_cast(data); + int code = (int)calldata_int(params, "code"); + + output->virtualCamActive = false; + os_atomic_set_bool(&virtualcam_active, false); + QMetaObject::invokeMethod(output->main, "OnVirtualCamStop", Q_ARG(int, code)); +} + +static void OBSDeactivateVirtualCam(void *data, calldata_t * /* params */) +{ + BasicOutputHandler *output = static_cast(data); + output->DestroyVirtualCamView(); +} + +/* ------------------------------------------------------------------------ */ + +struct StartMultitrackVideoStreamingGuard { + StartMultitrackVideoStreamingGuard() { future = guard.get_future().share(); }; + ~StartMultitrackVideoStreamingGuard() { guard.set_value(); } + + std::shared_future GetFuture() const { return future; } + + static std::shared_future MakeReadyFuture() + { + StartMultitrackVideoStreamingGuard guard; + return guard.GetFuture(); + } + +private: + std::promise guard; + std::shared_future future; +}; + +/* ------------------------------------------------------------------------ */ + +static bool CreateSimpleAACEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleAACEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static bool CreateSimpleOpusEncoder(OBSEncoder &res, int bitrate, const char *name, size_t idx) +{ + const char *id_ = GetSimpleOpusEncoderForBitrate(bitrate); + if (!id_) { + res = nullptr; + return false; + } + + res = obs_audio_encoder_create(id_, name, nullptr, idx, nullptr); + + if (res) { + obs_encoder_release(res); + return true; + } + + return false; +} + +static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1, + const char *prot_test2 = nullptr) +{ + return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) && + (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0; +} + +static bool return_first_id(void *data, const char *id) +{ + const char **output = (const char **)data; + + *output = id; + return false; +} + +static const char *GetStreamOutputType(const obs_service_t *service) +{ + const char *protocol = obs_service_get_protocol(service); + const char *output = nullptr; + + if (!protocol) { + blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); + return nullptr; + } + + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); + return nullptr; + } + + /* Check if the service has a preferred output type */ + output = obs_service_get_preferred_output_type(service); + if (output) { + if ((obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) + return output; + + blog(LOG_WARNING, "The output '%s' is not registered, fallback to another one", output); + } + + /* Otherwise, prefer first-party output types */ + if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { + return "rtmp_output"; + } else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) { + return "ffmpeg_hls_muxer"; + } else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) { + return "ffmpeg_mpegts_muxer"; + } + + /* If third-party protocol, use the first enumerated type */ + obs_enum_output_types_with_protocol(protocol, &output, return_first_id); + if (output) + return output; + + blog(LOG_WARNING, "No output compatible with the service '%s' is registered", obs_service_get_id(service)); + + return nullptr; +} + +/* ------------------------------------------------------------------------ */ + +inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) +{ + if (main->vcamEnabled) { + virtualCam = obs_output_create(VIRTUAL_CAM_ID, "virtualcam_output", nullptr, nullptr); + + signal_handler_t *signal = obs_output_get_signal_handler(virtualCam); + startVirtualCam.Connect(signal, "start", OBSStartVirtualCam, this); + stopVirtualCam.Connect(signal, "stop", OBSStopVirtualCam, this); + deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); + } + + auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); + if (!config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")) { + auto service = main_->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + multitrack_enabled = obs_data_has_user_value(settings, "multitrack_video_configuration_url"); + } + if (multitrack_enabled) + multitrackVideo = make_unique(); +} + +extern void log_vcam_changed(const VCamConfig &config, bool starting); + +bool BasicOutputHandler::StartVirtualCam() +{ + if (!main->vcamEnabled) + return false; + + bool typeIsProgram = main->vcamConfig.type == VCamOutputType::ProgramView; + + if (!virtualCamView && !typeIsProgram) + virtualCamView = obs_view_create(); + + UpdateVirtualCamOutputSource(); + + if (!virtualCamVideo) { + virtualCamVideo = typeIsProgram ? obs_get_video() : obs_view_add(virtualCamView); + + if (!virtualCamVideo) + return false; + } + + obs_output_set_media(virtualCam, virtualCamVideo, obs_get_audio()); + if (!Active()) + SetupOutputs(); + + bool success = obs_output_start(virtualCam); + if (!success) { + QString errorReason; + + const char *error = obs_output_get_last_error(virtualCam); + if (error) { + errorReason = QT_UTF8(error); + } else { + errorReason = QTStr("Output.StartFailedGeneric"); + } + + QMessageBox::critical(main, QTStr("Output.StartVirtualCamFailed"), errorReason); + + DestroyVirtualCamView(); + } + + log_vcam_changed(main->vcamConfig, true); + + return success; +} + +void BasicOutputHandler::StopVirtualCam() +{ + if (main->vcamEnabled) { + obs_output_stop(virtualCam); + } +} + +bool BasicOutputHandler::VirtualCamActive() const +{ + if (main->vcamEnabled) { + return obs_output_active(virtualCam); + } + return false; +} + +void BasicOutputHandler::UpdateVirtualCamOutputSource() +{ + if (!main->vcamEnabled || !virtualCamView) + return; + + OBSSourceAutoRelease source; + + switch (main->vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + DestroyVirtualCameraScene(); + return; + case VCamOutputType::PreviewOutput: { + DestroyVirtualCameraScene(); + OBSSource s = main->GetCurrentSceneSource(); + obs_source_get_ref(s); + source = s.Get(); + break; + } + case VCamOutputType::SceneOutput: + DestroyVirtualCameraScene(); + source = obs_get_source_by_name(main->vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + OBSSourceAutoRelease s = obs_get_source_by_name(main->vcamConfig.source.c_str()); + + if (!vCamSourceScene) + vCamSourceScene = obs_scene_create_private("vcam_source"); + source = obs_source_get_ref(obs_scene_get_source(vCamSourceScene)); + + if (vCamSourceSceneItem && (obs_sceneitem_get_source(vCamSourceSceneItem) != s)) { + obs_sceneitem_remove(vCamSourceSceneItem); + vCamSourceSceneItem = nullptr; + } + + if (!vCamSourceSceneItem) { + vCamSourceSceneItem = obs_scene_add(vCamSourceScene, s); + + obs_sceneitem_set_bounds_type(vCamSourceSceneItem, OBS_BOUNDS_SCALE_INNER); + obs_sceneitem_set_bounds_alignment(vCamSourceSceneItem, OBS_ALIGN_CENTER); + + const struct vec2 size = { + (float)obs_source_get_width(source), + (float)obs_source_get_height(source), + }; + obs_sceneitem_set_bounds(vCamSourceSceneItem, &size); + } + break; + } + + OBSSourceAutoRelease current = obs_view_get_source(virtualCamView, 0); + if (source != current) + obs_view_set_source(virtualCamView, 0, source); +} + +void BasicOutputHandler::DestroyVirtualCamView() +{ + if (main->vcamConfig.type == VCamOutputType::ProgramView) { + virtualCamVideo = nullptr; + return; + } + + obs_view_remove(virtualCamView); + obs_view_set_source(virtualCamView, 0, nullptr); + virtualCamVideo = nullptr; + + obs_view_destroy(virtualCamView); + virtualCamView = nullptr; + + DestroyVirtualCameraScene(); +} + +void BasicOutputHandler::DestroyVirtualCameraScene() +{ + if (!vCamSourceScene) + return; + + obs_scene_release(vCamSourceScene); + vCamSourceScene = nullptr; + vCamSourceSceneItem = nullptr; +} + +/* ------------------------------------------------------------------------ */ + +struct SimpleOutput : BasicOutputHandler { + OBSEncoder audioStreaming; + OBSEncoder videoStreaming; + OBSEncoder audioRecording; + OBSEncoder audioArchive; + OBSEncoder videoRecording; + OBSEncoder audioTrack[MAX_AUDIO_MIXES]; + + string videoEncoder; + string videoQuality; + bool usingRecordingPreset = false; + bool recordingConfigured = false; + bool ffmpegOutput = false; + bool lowCPUx264 = false; + + SimpleOutput(OBSBasic *main_); + + int CalcCRF(int crf); + + void UpdateRecordingSettings_x264_crf(int crf); + void UpdateRecordingSettings_qsv11(int crf, bool av1); + void UpdateRecordingSettings_nvenc(int cqp); + void UpdateRecordingSettings_nvenc_hevc_av1(int cqp); + void UpdateRecordingSettings_amd_cqp(int cqp); + void UpdateRecordingSettings_apple(int quality); +#ifdef ENABLE_HEVC + void UpdateRecordingSettings_apple_hevc(int quality); +#endif + void UpdateRecordingSettings(); + void UpdateRecordingAudioSettings(); + virtual void Update() override; + + void SetupOutputs() override; + int GetAudioBitrate() const; + + void LoadRecordingPreset_Lossy(const char *encoder); + void LoadRecordingPreset_Lossless(); + void LoadRecordingPreset(); + + void LoadStreamingPreset_Lossy(const char *encoder); + + void UpdateRecording(); + bool ConfigureRecording(bool useReplayBuffer); + + bool IsVodTrackEnabled(obs_service_t *service); + void SetupVodTrack(obs_service_t *service); + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; +}; + +void SimpleOutput::LoadRecordingPreset_Lossless() +{ + fileOutput = obs_output_create("ffmpeg_output", "simple_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(simple output)"; + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "format_name", "avi"); + obs_data_set_string(settings, "video_encoder", "utvideo"); + obs_data_set_string(settings, "audio_encoder", "pcm_s16le"); + + obs_output_update(fileOutput, settings); +} + +void SimpleOutput::LoadRecordingPreset_Lossy(const char *encoderId) +{ + videoRecording = obs_video_encoder_create(encoderId, "simple_video_recording", nullptr, nullptr); + if (!videoRecording) + throw "Failed to create video recording encoder (simple output)"; + obs_encoder_release(videoRecording); +} + +void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) +{ + videoStreaming = obs_video_encoder_create(encoderId, "simple_video_stream", nullptr, nullptr); + if (!videoStreaming) + throw "Failed to create video streaming encoder (simple output)"; + obs_encoder_release(videoStreaming); +} + +/* mistakes have been made to lead us to this. */ +const char *get_simple_output_encoder(const char *encoder) +{ + if (strcmp(encoder, SIMPLE_ENCODER_X264) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) { + return "obs_x264"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + return "obs_qsv11_v2"; + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + return "obs_qsv11_av1"; + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + return "h264_texture_amf"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + return "h265_texture_amf"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + return "av1_texture_amf"; + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + return EncoderAvailable("obs_nvenc_h264_tex") ? "obs_nvenc_h264_tex" : "ffmpeg_nvenc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + return EncoderAvailable("obs_nvenc_hevc_tex") ? "obs_nvenc_hevc_tex" : "ffmpeg_hevc_nvenc"; +#endif + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + return "obs_nvenc_av1_tex"; + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_H264) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.avc"; +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_APPLE_HEVC) == 0) { + return "com.apple.videotoolbox.videoencoder.ave.hevc"; +#endif + } + + return "obs_x264"; +} + +void SimpleOutput::LoadRecordingPreset() +{ + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "RecEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "RecAudioEncoder"); + + videoEncoder = encoder; + videoQuality = quality; + ffmpegOutput = false; + + if (strcmp(quality, "Stream") == 0) { + videoRecording = videoStreaming; + audioRecording = audioStreaming; + usingRecordingPreset = false; + return; + + } else if (strcmp(quality, "Lossless") == 0) { + LoadRecordingPreset_Lossless(); + usingRecordingPreset = true; + ffmpegOutput = true; + return; + + } else { + lowCPUx264 = false; + + if (strcmp(encoder, SIMPLE_ENCODER_X264_LOWCPU) == 0) + lowCPUx264 = true; + LoadRecordingPreset_Lossy(get_simple_output_encoder(encoder)); + usingRecordingPreset = true; + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioRecording, 192, "simple_opus_recording", 0); + else + success = CreateSimpleAACEncoder(audioRecording, 192, "simple_aac_recording", 0); + + if (!success) + throw "Failed to create audio recording encoder " + "(simple output)"; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[23]; + if (strcmp(audio_encoder, "opus") == 0) { + snprintf(name, sizeof name, "simple_opus_recording%d", i); + success = CreateSimpleOpusEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } else { + snprintf(name, sizeof name, "simple_aac_recording%d", i); + success = CreateSimpleAACEncoder(audioTrack[i], GetAudioBitrate(), name, i); + } + if (!success) + throw "Failed to create multi-track audio recording encoder " + "(simple output)"; + } + } +} + +#define SIMPLE_ARCHIVE_NAME "simple_archive_audio" + +SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + + LoadStreamingPreset_Lossy(get_simple_output_encoder(encoder)); + + bool success = false; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioStreaming, GetAudioBitrate(), "simple_opus", 0); + else + success = CreateSimpleAACEncoder(audioStreaming, GetAudioBitrate(), "simple_aac", 0); + + if (!success) + throw "Failed to create audio streaming encoder (simple output)"; + + if (strcmp(audio_encoder, "opus") == 0) + success = CreateSimpleOpusEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + else + success = CreateSimpleAACEncoder(audioArchive, GetAudioBitrate(), SIMPLE_ARCHIVE_NAME, 1); + + if (!success) + throw "Failed to create audio archive encoder (simple output)"; + + LoadRecordingPreset(); + + if (!ffmpegOutput) { + bool useReplayBuffer = config_get_bool(main->Config(), "SimpleOutput", "RecRB"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool use_native = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(use_native ? "mp4_output" : "ffmpeg_muxer", "simple_file_output", + nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(simple output)"; + } + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); +} + +int SimpleOutput::GetAudioBitrate() const +{ + const char *audio_encoder = config_get_string(main->Config(), "SimpleOutput", "StreamAudioEncoder"); + int bitrate = (int)config_get_uint(main->Config(), "SimpleOutput", "ABitrate"); + + if (strcmp(audio_encoder, "opus") == 0) + return FindClosestAvailableSimpleOpusBitrate(bitrate); + + return FindClosestAvailableSimpleAACBitrate(bitrate); +} + +void SimpleOutput::Update() +{ + OBSDataAutoRelease videoSettings = obs_data_create(); + OBSDataAutoRelease audioSettings = obs_data_create(); + + int videoBitrate = config_get_uint(main->Config(), "SimpleOutput", "VBitrate"); + int audioBitrate = GetAudioBitrate(); + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + const char *custom = config_get_string(main->Config(), "SimpleOutput", "x264Settings"); + const char *encoder = config_get_string(main->Config(), "SimpleOutput", "StreamEncoder"); + const char *encoder_id = obs_encoder_get_id(videoStreaming); + const char *presetType; + const char *preset; + + if (strcmp(encoder, SIMPLE_ENCODER_QSV) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_QSV_AV1) == 0) { + presetType = "QSVPreset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD) == 0) { + presetType = "AMDPreset"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_HEVC) == 0) { + presetType = "AMDPreset"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC) == 0) { + presetType = "NVENCPreset2"; + +#ifdef ENABLE_HEVC + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_HEVC) == 0) { + presetType = "NVENCPreset2"; +#endif + + } else if (strcmp(encoder, SIMPLE_ENCODER_AMD_AV1) == 0) { + presetType = "AMDAV1Preset"; + + } else if (strcmp(encoder, SIMPLE_ENCODER_NVENC_AV1) == 0) { + presetType = "NVENCPreset2"; + + } else { + presetType = "Preset"; + } + + preset = config_get_string(main->Config(), "SimpleOutput", presetType); + + /* Only use preset2 for legacy/FFmpeg NVENC Encoder. */ + if (strncmp(encoder_id, "ffmpeg_", 7) == 0 && strcmp(presetType, "NVENCPreset2") == 0) { + obs_data_set_string(videoSettings, "preset2", preset); + } else { + obs_data_set_string(videoSettings, "preset", preset); + } + + obs_data_set_string(videoSettings, "rate_control", "CBR"); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + + if (advanced) + obs_data_set_string(videoSettings, "x264opts", custom); + + obs_data_set_string(audioSettings, "rate_control", "CBR"); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + + obs_service_apply_encoder_settings(main->GetService(), videoSettings, audioSettings); + + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + obs_data_set_int(audioSettings, "bitrate", audioBitrate); + } + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, videoSettings); + obs_encoder_update(audioStreaming, audioSettings); + obs_encoder_update(audioArchive, audioSettings); +} + +void SimpleOutput::UpdateRecordingAudioSettings() +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", 192); + obs_data_set_string(settings, "rate_control", "CBR"); + + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv || strcmp(quality, "Stream") == 0) { + obs_encoder_update(audioRecording, settings); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_update(audioTrack[i], settings); + } + } + } +} + +#define CROSS_DIST_CUTOFF 2000.0 + +int SimpleOutput::CalcCRF(int crf) +{ + int cx = config_get_uint(main->Config(), "Video", "OutputCX"); + int cy = config_get_uint(main->Config(), "Video", "OutputCY"); + double fCX = double(cx); + double fCY = double(cy); + + if (lowCPUx264) + crf -= 2; + + double crossDist = sqrt(fCX * fCX + fCY * fCY); + double crfResReduction = fmin(CROSS_DIST_CUTOFF, crossDist) / CROSS_DIST_CUTOFF; + crfResReduction = (1.0 - crfResReduction) * 10.0; + + return crf - int(crfResReduction); +} + +void SimpleOutput::UpdateRecordingSettings_x264_crf(int crf) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "crf", crf); + obs_data_set_bool(settings, "use_bufsize", true); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", lowCPUx264 ? "ultrafast" : "veryfast"); + + obs_encoder_update(videoRecording, settings); +} + +static bool icq_available(obs_encoder_t *encoder) +{ + obs_properties_t *props = obs_encoder_properties(encoder); + obs_property_t *p = obs_properties_get(props, "rate_control"); + bool icq_found = false; + + size_t num = obs_property_list_item_count(p); + for (size_t i = 0; i < num; i++) { + const char *val = obs_property_list_item_string(p, i); + if (strcmp(val, "ICQ") == 0) { + icq_found = true; + break; + } + } + + obs_properties_destroy(props); + return icq_found; +} + +void SimpleOutput::UpdateRecordingSettings_qsv11(int crf, bool av1) +{ + bool icq = icq_available(videoRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "profile", "high"); + + if (icq && !av1) { + obs_data_set_string(settings, "rate_control", "ICQ"); + obs_data_set_int(settings, "icq_quality", crf); + } else { + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_int(settings, "cqp", crf); + } + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_nvenc_hevc_av1(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "cqp", cqp); + + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings_apple(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} + +#ifdef ENABLE_HEVC +void SimpleOutput::UpdateRecordingSettings_apple_hevc(int quality) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CRF"); + obs_data_set_string(settings, "profile", "main"); + obs_data_set_int(settings, "quality", quality); + + obs_encoder_update(videoRecording, settings); +} +#endif + +void SimpleOutput::UpdateRecordingSettings_amd_cqp(int cqp) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "rate_control", "CQP"); + obs_data_set_string(settings, "profile", "high"); + obs_data_set_string(settings, "preset", "quality"); + obs_data_set_int(settings, "cqp", cqp); + obs_encoder_update(videoRecording, settings); +} + +void SimpleOutput::UpdateRecordingSettings() +{ + bool ultra_hq = (videoQuality == "HQ"); + int crf = CalcCRF(ultra_hq ? 16 : 23); + + if (astrcmp_n(videoEncoder.c_str(), "x264", 4) == 0) { + UpdateRecordingSettings_x264_crf(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV) { + UpdateRecordingSettings_qsv11(crf, false); + + } else if (videoEncoder == SIMPLE_ENCODER_QSV_AV1) { + UpdateRecordingSettings_qsv11(crf, true); + + } else if (videoEncoder == SIMPLE_ENCODER_AMD) { + UpdateRecordingSettings_amd_cqp(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_AMD_HEVC) { + UpdateRecordingSettings_amd_cqp(crf); +#endif + + } else if (videoEncoder == SIMPLE_ENCODER_AMD_AV1) { + UpdateRecordingSettings_amd_cqp(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_NVENC) { + UpdateRecordingSettings_nvenc(crf); + +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_HEVC) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); +#endif + } else if (videoEncoder == SIMPLE_ENCODER_NVENC_AV1) { + UpdateRecordingSettings_nvenc_hevc_av1(crf); + + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_H264) { + /* These are magic numbers. 0 - 100, more is better. */ + UpdateRecordingSettings_apple(ultra_hq ? 70 : 50); +#ifdef ENABLE_HEVC + } else if (videoEncoder == SIMPLE_ENCODER_APPLE_HEVC) { + UpdateRecordingSettings_apple_hevc(ultra_hq ? 70 : 50); +#endif + } + UpdateRecordingAudioSettings(); +} + +inline void SimpleOutput::SetupOutputs() +{ + SimpleOutput::Update(); + obs_encoder_set_video(videoStreaming, obs_get_video()); + obs_encoder_set_audio(audioStreaming, obs_get_audio()); + obs_encoder_set_audio(audioArchive, obs_get_audio()); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + + if (usingRecordingPreset) { + if (ffmpegOutput) { + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + } else { + obs_encoder_set_video(videoRecording, obs_get_video()); + if (flv) { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_encoder_set_audio(audioTrack[i], obs_get_audio()); + } + } + } + } + } else { + obs_encoder_set_audio(audioRecording, obs_get_audio()); + } +} + +const char *FindAudioEncoderFromCodec(const char *type) +{ + const char *alt_enc_id = nullptr; + size_t i = 0; + + while (obs_enum_encoder_types(i++, &alt_enc_id)) { + const char *codec = obs_get_encoder_codec(alt_enc_id); + if (strcmp(type, codec) == 0) { + return alt_enc_id; + } + } + + return nullptr; +} + +std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, SetupStreamingContinuation_t continuation) +{ + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + auto audio_bitrate = GetAudioBitrate(); + auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} : std::nullopt; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "simple_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", + OBSStreamStopping, this); + + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, + this); + stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); + obs_output_set_service(streamOutput, service); + return true; + }; + + return SetupMultitrackVideo(service, GetSimpleAACEncoderForBitrate(audio_bitrate), 0, vod_track_mixer, + [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +static inline bool ServiceSupportsVodTrack(const char *service); + +static void clear_archive_encoder(obs_output_t *output, const char *expected_name) +{ + obs_encoder_t *last = obs_output_get_audio_encoder(output, 1); + bool clear = false; + + /* ensures that we don't remove twitch's soundtrack encoder */ + if (last) { + const char *name = obs_encoder_get_name(last); + clear = name && strcmp(name, expected_name) == 0; + obs_encoder_release(last); + } + + if (clear) + obs_output_set_audio_encoder(output, nullptr, 1); +} + +bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) +{ + bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); + bool enable = config_get_bool(main->Config(), "SimpleOutput", "VodTrackEnabled"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *name = obs_data_get_string(settings, "service"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) + return enableForCustomServer ? enable : false; + else + return advanced && enable && ServiceSupportsVodTrack(name); +} + +void SimpleOutput::SetupVodTrack(obs_service_t *service) +{ + if (IsVodTrackEnabled(service)) + obs_output_set_audio_encoder(streamOutput, audioArchive, 1); + else + clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); +} + +bool SimpleOutput::StartStreaming(obs_service_t *service) +{ + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_uint(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_uint(main->Config(), "Output", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + if (!reconnect) + maxRetries = 0; + + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + + if (!multitrackVideo || !multitrackVideoActive) + SetupVodTrack(service); + + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +void SimpleOutput::UpdateRecording() +{ + const char *recFormat = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + bool flv = strcmp(recFormat, "flv") == 0; + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + int idx = 0; + int idx2 = 0; + const char *quality = config_get_string(main->Config(), "SimpleOutput", "RecQuality"); + + if (replayBufferActive || recordingActive) + return; + + if (usingRecordingPreset) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(streamOutput)) { + Update(); + } + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput) { + obs_output_set_video_encoder(fileOutput, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(fileOutput, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, audioTrack[i], idx++); + } + } + } + } + if (replayBuffer) { + obs_output_set_video_encoder(replayBuffer, videoRecording); + if (flv || strcmp(quality, "Stream") == 0) { + obs_output_set_audio_encoder(replayBuffer, audioRecording, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(replayBuffer, audioTrack[i], idx2++); + } + } + } + } + + recordingConfigured = true; +} + +bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer) +{ + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + const char *format = config_get_string(main->Config(), "SimpleOutput", "RecFormat2"); + const char *mux = config_get_string(main->Config(), "SimpleOutput", "MuxerCustom"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + const char *rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + const char *rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + int rbTime = config_get_int(main->Config(), "SimpleOutput", "RecRBTime"); + int rbSize = config_get_int(main->Config(), "SimpleOutput", "RecRBSize"); + int tracks = config_get_int(main->Config(), "SimpleOutput", "RecTracks"); + + bool is_fragmented = strncmp(format, "fragmented", 10) == 0; + bool is_lossless = videoQuality == "Lossless"; + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + if (updateReplayBuffer) { + f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(format); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usingRecordingPreset ? rbSize : 0); + } else { + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, ffmpegOutput ? "avi" : format, noSpace, overwriteIfExists, + f.c_str(), ffmpegOutput); + obs_data_set_string(settings, ffmpegOutput ? "url" : "path", strPath.c_str()); + if (ffmpegOutput) + obs_output_set_mixers(fileOutput, tracks); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && !is_lossless && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented && !is_lossless) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + if (updateReplayBuffer) + obs_output_update(replayBuffer, settings); + else + obs_output_update(fileOutput, settings); + + return true; +} + +bool SimpleOutput::StartRecording() +{ + UpdateRecording(); + if (!ConfigureRecording(false)) + return false; + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool SimpleOutput::StartReplayBuffer() +{ + UpdateRecording(); + if (!ConfigureRecording(true)) + return false; + if (!obs_output_start(replayBuffer)) { + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), QTStr("Output.StartFailedGeneric")); + return false; + } + + return true; +} + +void SimpleOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void SimpleOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void SimpleOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool SimpleOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool SimpleOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool SimpleOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +struct AdvancedOutput : BasicOutputHandler { + OBSEncoder streamAudioEnc; + OBSEncoder streamArchiveEnc; + OBSEncoder streamTrack[MAX_AUDIO_MIXES]; + OBSEncoder recordTrack[MAX_AUDIO_MIXES]; + OBSEncoder videoStreaming; + OBSEncoder videoRecording; + + bool ffmpegOutput; + bool ffmpegRecording; + bool useStreamEncoder; + bool useStreamAudioEncoder; + bool usesBitrate = false; + + AdvancedOutput(OBSBasic *main_); + + inline void UpdateStreamSettings(); + inline void UpdateRecordingSettings(); + inline void UpdateAudioSettings(); + virtual void Update() override; + + inline std::optional VodTrackMixerIdx(obs_service_t *service); + inline void SetupVodTrack(obs_service_t *service); + + inline void SetupStreaming(); + inline void SetupRecording(); + inline void SetupFFmpeg(); + void SetupOutputs() override; + int GetAudioBitrate(size_t i, const char *id) const; + + virtual std::shared_future SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) override; + virtual bool StartStreaming(obs_service_t *service) override; + virtual bool StartRecording() override; + virtual bool StartReplayBuffer() override; + virtual void StopStreaming(bool force) override; + virtual void StopRecording(bool force) override; + virtual void StopReplayBuffer(bool force) override; + virtual bool StreamingActive() const override; + virtual bool RecordingActive() const override; + virtual bool ReplayBufferActive() const override; + bool allowsMultiTrack(); +}; + +static OBSData GetDataFromJsonFile(const char *jsonFile) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(jsonFile); + + OBSDataAutoRelease data = nullptr; + + if (!jsonFilePath.empty()) { + BPtr jsonData = os_quick_read_utf8_file(jsonFilePath.u8string().c_str()); + + if (!!jsonData) { + data = obs_data_create_from_json(jsonData); + } + } + + if (!data) { + data = obs_data_create(); + } + + return data.Get(); +} + +static void ApplyEncoderDefaults(OBSData &settings, const obs_encoder_t *encoder) +{ + OBSData dataRet = obs_encoder_get_defaults(encoder); + obs_data_release(dataRet); + + if (!!settings) + obs_data_apply(dataRet, settings); + settings = std::move(dataRet); +} + +#define ADV_ARCHIVE_NAME "adv_archive_audio" + +#ifdef __APPLE__ +static void translate_macvth264_encoder(const char *&encoder) +{ + if (strcmp(encoder, "vt_h264_hw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264.gva"; + } else if (strcmp(encoder, "vt_h264_sw") == 0) { + encoder = "com.apple.videotoolbox.videoencoder.h264"; + } +} +#endif + +AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) +{ + const char *recType = config_get_string(main->Config(), "AdvOut", "RecType"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + const char *streamAudioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recordEncoder = config_get_string(main->Config(), "AdvOut", "RecEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); +#ifdef __APPLE__ + translate_macvth264_encoder(streamEncoder); + translate_macvth264_encoder(recordEncoder); +#endif + + ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + ffmpegRecording = ffmpegOutput && config_get_bool(main->Config(), "AdvOut", "FFOutputToFile"); + useStreamEncoder = astrcmpi(recordEncoder, "none") == 0; + useStreamAudioEncoder = astrcmpi(recAudioEncoder, "none") == 0; + + OBSData streamEncSettings = GetDataFromJsonFile("streamEncoder.json"); + OBSData recordEncSettings = GetDataFromJsonFile("recordEncoder.json"); + + if (ffmpegOutput) { + fileOutput = obs_output_create("ffmpeg_output", "adv_ffmpeg_output", nullptr, nullptr); + if (!fileOutput) + throw "Failed to create recording FFmpeg output " + "(advanced output)"; + } else { + bool useReplayBuffer = config_get_bool(main->Config(), "AdvOut", "RecRB"); + if (useReplayBuffer) { + OBSDataAutoRelease hotkey; + const char *str = config_get_string(main->Config(), "Hotkeys", "ReplayBuffer"); + if (str) + hotkey = obs_data_create_from_json(str); + else + hotkey = nullptr; + + replayBuffer = obs_output_create("replay_buffer", Str("ReplayBuffer"), nullptr, hotkey); + + if (!replayBuffer) + throw "Failed to create replay buffer output " + "(simple output)"; + + signal_handler_t *signal = obs_output_get_signal_handler(replayBuffer); + + startReplayBuffer.Connect(signal, "start", OBSStartReplayBuffer, this); + stopReplayBuffer.Connect(signal, "stop", OBSStopReplayBuffer, this); + replayBufferStopping.Connect(signal, "stopping", OBSReplayBufferStopping, this); + replayBufferSaved.Connect(signal, "saved", OBSReplayBufferSaved, this); + } + + bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0; + fileOutput = obs_output_create(native_muxer ? "mp4_output" : "ffmpeg_muxer", "adv_file_output", nullptr, + nullptr); + if (!fileOutput) + throw "Failed to create recording output " + "(advanced output)"; + + if (!useStreamEncoder) { + videoRecording = obs_video_encoder_create(recordEncoder, "advanced_video_recording", + recordEncSettings, nullptr); + if (!videoRecording) + throw "Failed to create recording video " + "encoder (advanced output)"; + obs_encoder_release(videoRecording); + } + } + + videoStreaming = obs_video_encoder_create(streamEncoder, "advanced_video_stream", streamEncSettings, nullptr); + if (!videoStreaming) + throw "Failed to create streaming video encoder " + "(advanced output)"; + obs_encoder_release(videoStreaming); + + const char *rate_control = + obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); + if (!rate_control) + rate_control = ""; + usesBitrate = astrcmpi(rate_control, "CBR") == 0 || astrcmpi(rate_control, "VBR") == 0 || + astrcmpi(rate_control, "ABR") == 0; + + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + char name[19]; + snprintf(name, sizeof(name), "adv_record_audio_%d", i); + + recordTrack[i] = obs_audio_encoder_create(useStreamAudioEncoder ? streamAudioEncoder : recAudioEncoder, + name, nullptr, i, nullptr); + + if (!recordTrack[i]) { + throw "Failed to create audio encoder " + "(advanced output)"; + } + + obs_encoder_release(recordTrack[i]); + + snprintf(name, sizeof(name), "adv_stream_audio_%d", i); + streamTrack[i] = obs_audio_encoder_create(streamAudioEncoder, name, nullptr, i, nullptr); + + if (!streamTrack[i]) { + throw "Failed to create streaming audio encoders " + "(advanced output)"; + } + + obs_encoder_release(streamTrack[i]); + } + + std::string id; + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + streamAudioEnc = + obs_audio_encoder_create(streamAudioEncoder, "adv_stream_audio", nullptr, streamTrackIndex, nullptr); + if (!streamAudioEnc) + throw "Failed to create streaming audio encoder " + "(advanced output)"; + obs_encoder_release(streamAudioEnc); + + id = ""; + int vodTrack = config_get_int(main->Config(), "AdvOut", "VodTrackIndex") - 1; + streamArchiveEnc = obs_audio_encoder_create(streamAudioEncoder, ADV_ARCHIVE_NAME, nullptr, vodTrack, nullptr); + if (!streamArchiveEnc) + throw "Failed to create archive audio encoder " + "(advanced output)"; + obs_encoder_release(streamArchiveEnc); + + startRecording.Connect(obs_output_get_signal_handler(fileOutput), "start", OBSStartRecording, this); + stopRecording.Connect(obs_output_get_signal_handler(fileOutput), "stop", OBSStopRecording, this); + recordStopping.Connect(obs_output_get_signal_handler(fileOutput), "stopping", OBSRecordStopping, this); + recordFileChanged.Connect(obs_output_get_signal_handler(fileOutput), "file_changed", OBSRecordFileChanged, + this); +} + +void AdvancedOutput::UpdateStreamSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + bool dynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + const char *streamEncoder = config_get_string(main->Config(), "AdvOut", "Encoder"); + + OBSData settings = GetDataFromJsonFile("streamEncoder.json"); + ApplyEncoderDefaults(settings, videoStreaming); + + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings, "bitrate"); + int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + if (!enforceBitrate) { + blog(LOG_INFO, "User is ignoring service bitrate limits."); + obs_data_set_int(settings, "bitrate", bitrate); + } + + int enforced_keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + if (keyint_sec != 0 && keyint_sec < enforced_keyint_sec) + obs_data_set_int(settings, "keyint_sec", keyint_sec); + } else { + blog(LOG_WARNING, "User is ignoring service settings."); + } + + if (dynBitrate && strstr(streamEncoder, "nvenc") != nullptr) + obs_data_set_bool(settings, "lookahead", false); + + video_t *video = obs_get_video(); + enum video_format format = video_output_get_format(video); + + switch (format) { + case VIDEO_FORMAT_I420: + case VIDEO_FORMAT_NV12: + case VIDEO_FORMAT_I010: + case VIDEO_FORMAT_P010: + break; + default: + obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + } + + obs_encoder_update(videoStreaming, settings); +} + +inline void AdvancedOutput::UpdateRecordingSettings() +{ + OBSData settings = GetDataFromJsonFile("recordEncoder.json"); + obs_encoder_update(videoRecording, settings); +} + +void AdvancedOutput::Update() +{ + UpdateStreamSettings(); + if (!useStreamEncoder && !ffmpegOutput) + UpdateRecordingSettings(); + UpdateAudioSettings(); +} + +static inline bool ServiceSupportsVodTrack(const char *service) +{ + static const char *vodTrackServices[] = {"Twitch"}; + + for (const char *vodTrackService : vodTrackServices) { + if (astrcmpi(vodTrackService, service) == 0) + return true; + } + + return false; +} + +inline bool AdvancedOutput::allowsMultiTrack() +{ + const char *protocol = nullptr; + obs_service_t *service_obj = main->GetService(); + protocol = obs_service_get_protocol(service_obj); + if (!protocol) + return false; + return astrcmpi_n(protocol, SRT_PROTOCOL, strlen(SRT_PROTOCOL)) == 0 || + astrcmpi_n(protocol, RIST_PROTOCOL, strlen(RIST_PROTOCOL)) == 0; +} + +inline void AdvancedOutput::SetupStreaming() +{ + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + bool is_multitrack_output = allowsMultiTrack(); + + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + obs_encoder_set_scaled_size(videoStreaming, cx, cy); + obs_encoder_set_gpu_scale_type(videoStreaming, (obs_scale_type)rescaleFilter); + + const char *id = obs_service_get_id(main->GetService()); + if (strcmp(id, "rtmp_custom") == 0) { + OBSDataAutoRelease settings = obs_data_create(); + obs_service_apply_encoder_settings(main->GetService(), settings, nullptr); + obs_encoder_update(videoStreaming, settings); + } +} + +inline void AdvancedOutput::SetupRecording() +{ + const char *path = config_get_string(main->Config(), "AdvOut", "RecFilePath"); + const char *mux = config_get_string(main->Config(), "AdvOut", "RecMuxerCustom"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "RecRescaleRes"); + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RecRescaleFilter"); + int tracks; + + const char *recFormat = config_get_string(main->Config(), "AdvOut", "RecFormat2"); + + bool is_fragmented = strncmp(recFormat, "fragmented", 10) == 0; + bool flv = strcmp(recFormat, "flv") == 0; + + if (flv) + tracks = config_get_int(main->Config(), "AdvOut", "FLVTrack"); + else + tracks = config_get_int(main->Config(), "AdvOut", "RecTracks"); + + OBSDataAutoRelease settings = obs_data_create(); + unsigned int cx = 0; + unsigned int cy = 0; + int idx = 0; + + /* Hack to allow recordings without any audio tracks selected. It is no + * longer possible to select such a configuration in settings, but legacy + * configurations might still have this configured and we don't want to + * just break them. */ + if (tracks == 0) + tracks = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + + if (useStreamEncoder) { + obs_output_set_video_encoder(fileOutput, videoStreaming); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoStreaming); + } else { + if (rescaleFilter != OBS_SCALE_DISABLE && rescaleRes && *rescaleRes) { + if (sscanf(rescaleRes, "%ux%u", &cx, &cy) != 2) { + cx = 0; + cy = 0; + } + } + + obs_encoder_set_scaled_size(videoRecording, cx, cy); + obs_encoder_set_gpu_scale_type(videoRecording, (obs_scale_type)rescaleFilter); + obs_output_set_video_encoder(fileOutput, videoRecording); + if (replayBuffer) + obs_output_set_video_encoder(replayBuffer, videoRecording); + } + + if (!flv) { + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((tracks & (1 << i)) != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[i], idx); + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[i], idx); + idx++; + } + } + } else if (flv && tracks != 0) { + obs_output_set_audio_encoder(fileOutput, recordTrack[tracks - 1], idx); + + if (replayBuffer) + obs_output_set_audio_encoder(replayBuffer, recordTrack[tracks - 1], idx); + } + + // Use fragmented MOV/MP4 if user has not already specified custom movflags + if (is_fragmented && (!mux || strstr(mux, "movflags") == NULL)) { + string mux_frag = "movflags=frag_keyframe+empty_moov+delay_moov"; + if (mux) { + mux_frag += " "; + mux_frag += mux; + } + obs_data_set_string(settings, "muxer_settings", mux_frag.c_str()); + } else { + if (is_fragmented) + blog(LOG_WARNING, "User enabled fragmented recording, " + "but custom muxer settings contained movflags."); + obs_data_set_string(settings, "muxer_settings", mux); + } + + obs_data_set_string(settings, "path", path); + obs_output_update(fileOutput, settings); + if (replayBuffer) + obs_output_update(replayBuffer, settings); +} + +inline void AdvancedOutput::SetupFFmpeg() +{ + const char *url = config_get_string(main->Config(), "AdvOut", "FFURL"); + int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate"); + int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize"); + bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale"); + const char *rescaleRes = config_get_string(main->Config(), "AdvOut", "FFRescaleRes"); + const char *formatName = config_get_string(main->Config(), "AdvOut", "FFFormat"); + const char *mimeType = config_get_string(main->Config(), "AdvOut", "FFFormatMimeType"); + const char *muxCustom = config_get_string(main->Config(), "AdvOut", "FFMCustom"); + const char *vEncoder = config_get_string(main->Config(), "AdvOut", "FFVEncoder"); + int vEncoderId = config_get_int(main->Config(), "AdvOut", "FFVEncoderId"); + const char *vEncCustom = config_get_string(main->Config(), "AdvOut", "FFVCustom"); + int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate"); + int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes"); + const char *aEncoder = config_get_string(main->Config(), "AdvOut", "FFAEncoder"); + int aEncoderId = config_get_int(main->Config(), "AdvOut", "FFAEncoderId"); + const char *aEncCustom = config_get_string(main->Config(), "AdvOut", "FFACustom"); + + OBSDataArrayAutoRelease audio_names = obs_data_array_create(); + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + + const char *audioName = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + OBSDataAutoRelease item = obs_data_create(); + obs_data_set_string(item, "name", audioName); + obs_data_array_push_back(audio_names, item); + } + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_array(settings, "audio_names", audio_names); + obs_data_set_string(settings, "url", url); + obs_data_set_string(settings, "format_name", formatName); + obs_data_set_string(settings, "format_mime_type", mimeType); + obs_data_set_string(settings, "muxer_settings", muxCustom); + obs_data_set_int(settings, "gop_size", gopSize); + obs_data_set_int(settings, "video_bitrate", vBitrate); + obs_data_set_string(settings, "video_encoder", vEncoder); + obs_data_set_int(settings, "video_encoder_id", vEncoderId); + obs_data_set_string(settings, "video_settings", vEncCustom); + obs_data_set_int(settings, "audio_bitrate", aBitrate); + obs_data_set_string(settings, "audio_encoder", aEncoder); + obs_data_set_int(settings, "audio_encoder_id", aEncoderId); + obs_data_set_string(settings, "audio_settings", aEncCustom); + + if (rescale && rescaleRes && *rescaleRes) { + int width; + int height; + int val = sscanf(rescaleRes, "%dx%d", &width, &height); + + if (val == 2 && width && height) { + obs_data_set_int(settings, "scale_width", width); + obs_data_set_int(settings, "scale_height", height); + } + } + + obs_output_set_mixers(fileOutput, aMixes); + obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio()); + obs_output_update(fileOutput, settings); +} + +static inline void SetEncoderName(obs_encoder_t *encoder, const char *name, const char *defaultName) +{ + obs_encoder_set_name(encoder, (name && *name) ? name : defaultName); +} + +inline void AdvancedOutput::UpdateAudioSettings() +{ + bool applyServiceSettings = config_get_bool(main->Config(), "AdvOut", "ApplyServiceSettings"); + bool enforceBitrate = !config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + const char *audioEncoder = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + const char *recAudioEncoder = config_get_string(main->Config(), "AdvOut", "RecAudioEncoder"); + + bool is_multitrack_output = allowsMultiTrack(); + + OBSDataAutoRelease settings[MAX_AUDIO_MIXES]; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + string cfg_name = "Track"; + cfg_name += to_string((int)i + 1); + cfg_name += "Name"; + const char *name = config_get_string(main->Config(), "AdvOut", cfg_name.c_str()); + + string def_name = "Track"; + def_name += to_string((int)i + 1); + SetEncoderName(recordTrack[i], name, def_name.c_str()); + SetEncoderName(streamTrack[i], name, def_name.c_str()); + } + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + int track = (int)(i + 1); + settings[i] = obs_data_create(); + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, recAudioEncoder)); + + obs_encoder_update(recordTrack[i], settings[i]); + + obs_data_set_int(settings[i], "bitrate", GetAudioBitrate(i, audioEncoder)); + + if (!is_multitrack_output) { + if (track == streamTrackIndex || track == vodTrackIndex) { + if (applyServiceSettings) { + int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); + obs_service_apply_encoder_settings(main->GetService(), nullptr, settings[i]); + + if (!enforceBitrate) + obs_data_set_int(settings[i], "bitrate", bitrate); + } + } + + if (track == streamTrackIndex) + obs_encoder_update(streamAudioEnc, settings[i]); + if (track == vodTrackIndex) + obs_encoder_update(streamArchiveEnc, settings[i]); + } else { + obs_encoder_update(streamTrack[i], settings[i]); + } + } +} + +void AdvancedOutput::SetupOutputs() +{ + obs_encoder_set_video(videoStreaming, obs_get_video()); + if (videoRecording) + obs_encoder_set_video(videoRecording, obs_get_video()); + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + obs_encoder_set_audio(streamTrack[i], obs_get_audio()); + obs_encoder_set_audio(recordTrack[i], obs_get_audio()); + } + obs_encoder_set_audio(streamAudioEnc, obs_get_audio()); + obs_encoder_set_audio(streamArchiveEnc, obs_get_audio()); + + SetupStreaming(); + + if (ffmpegOutput) + SetupFFmpeg(); + else + SetupRecording(); +} + +int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const +{ + static const char *names[] = { + "Track1Bitrate", "Track2Bitrate", "Track3Bitrate", "Track4Bitrate", "Track5Bitrate", "Track6Bitrate", + }; + int bitrate = (int)config_get_uint(main->Config(), "AdvOut", names[i]); + return FindClosestAvailableAudioBitrate(id, bitrate); +} + +inline std::optional AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) +{ + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); + bool vodTrackEnabled = config_get_bool(main->Config(), "AdvOut", "VodTrackEnabled"); + int vodTrackIndex = config_get_int(main->Config(), "AdvOut", "VodTrackIndex"); + bool enableForCustomServer = config_get_bool(App()->GetUserConfig(), "General", "EnableCustomServerVodTrack"); + + const char *id = obs_service_get_id(service); + if (strcmp(id, "rtmp_custom") == 0) { + vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; + } else { + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *service = obs_data_get_string(settings, "service"); + if (!ServiceSupportsVodTrack(service)) + vodTrackEnabled = false; + } + + if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) + return {vodTrackIndex - 1}; + return std::nullopt; +} + +inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +{ + if (VodTrackMixerIdx(service).has_value()) + obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); + else + clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); +} + +std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, + SetupStreamingContinuation_t continuation) +{ + int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); + + bool is_multitrack_output = allowsMultiTrack(); + + if (!useStreamEncoder || (!ffmpegOutput && !obs_output_active(fileOutput))) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + Auth *auth = main->GetAuth(); + if (auth) + auth->OnStreamConfig(); + + /* --------------------- */ + + const char *type = GetStreamOutputType(service); + if (!type) { + continuation(false); + return StartMultitrackVideoStreamingGuard::MakeReadyFuture(); + } + + const char *audio_encoder_id = config_get_string(main->Config(), "AdvOut", "AudioEncoder"); + int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex") - 1; + + auto handle_multitrack_video_result = [=](std::optional multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); + + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + streamOutput = obs_output_create(type, "adv_stream", nullptr, nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } + + streamDelayStarting.Connect(obs_output_get_signal_handler(streamOutput), "starting", + OBSStreamStarting, this); + streamStopping.Connect(obs_output_get_signal_handler(streamOutput), "stopping", + OBSStreamStopping, this); + + startStreaming.Connect(obs_output_get_signal_handler(streamOutput), "start", OBSStartStreaming, + this); + stopStreaming.Connect(obs_output_get_signal_handler(streamOutput), "stop", OBSStopStreaming, + this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, videoStreaming); + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + + if (!is_multitrack_output) { + obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + } else { + int idx = 0; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + if ((multiTrackAudioMixes & (1 << i)) != 0) { + obs_output_set_audio_encoder(streamOutput, streamTrack[i], idx); + idx++; + } + } + } + + return true; + }; + + return SetupMultitrackVideo(service, audio_encoder_id, static_cast(streamTrackIndex), + VodTrackMixerIdx(service), [=](std::optional res) { + continuation(handle_multitrack_video_result(res)); + }); +} + +bool AdvancedOutput::StartStreaming(obs_service_t *service) +{ + obs_output_set_service(streamOutput, service); + + bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); + int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); + int maxRetries = config_get_int(main->Config(), "Output", "MaxRetries"); + bool useDelay = config_get_bool(main->Config(), "Output", "DelayEnable"); + int delaySec = config_get_int(main->Config(), "Output", "DelaySec"); + bool preserveDelay = config_get_bool(main->Config(), "Output", "DelayPreserve"); + const char *bindIP = config_get_string(main->Config(), "Output", "BindIP"); + const char *ipFamily = config_get_string(main->Config(), "Output", "IPFamily"); +#ifdef _WIN32 + bool enableNewSocketLoop = config_get_bool(main->Config(), "Output", "NewSocketLoopEnable"); + bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; +#endif + bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings(main, main->Config(), service, useDelay, enableNewSocketLoop, + enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + + bool is_rtmp = false; + obs_service_t *service_obj = main->GetService(); + const char *protocol = obs_service_get_protocol(service_obj); + if (protocol) { + if (astrcmpi_n(protocol, RTMP_PROTOCOL, strlen(RTMP_PROTOCOL)) == 0) + is_rtmp = true; + } + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "bind_ip", bindIP); + obs_data_set_string(settings, "ip_family", ipFamily); +#ifdef _WIN32 + obs_data_set_bool(settings, "new_socket_loop_enabled", enableNewSocketLoop); + obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); +#endif + obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = StreamingOutput(); // shadowing is sort of bad, but also convenient + + obs_output_update(streamOutput, settings); + + if (!reconnect) + maxRetries = 0; + + obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + + obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + if (is_rtmp) { + SetupVodTrack(service); + } + if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); + return true; + } + + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + + const char *error = obs_output_get_last_error(streamOutput); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_output_get_id(streamOutput); + blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + return false; +} + +bool AdvancedOutput::StartRecording() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + bool splitFile; + const char *splitFileType; + int splitFileTime; + int splitFileSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) { + UpdateRecordingSettings(); + } + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + splitFile = config_get_bool(main->Config(), "AdvOut", "RecSplitFile"); + + string strPath = GetRecordingFilename(path, recFormat, noSpace, overwriteIfExists, filenameFormat, + ffmpegRecording); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, ffmpegRecording ? "url" : "path", strPath.c_str()); + + if (splitFile) { + splitFileType = config_get_string(main->Config(), "AdvOut", "RecSplitFileType"); + splitFileTime = (astrcmpi(splitFileType, "Time") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileTime") + : 0; + splitFileSize = (astrcmpi(splitFileType, "Size") == 0) + ? config_get_int(main->Config(), "AdvOut", "RecSplitFileSize") + : 0; + string ext = GetFormatExt(recFormat); + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", filenameFormat); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_bool(settings, "allow_overwrite", overwriteIfExists); + obs_data_set_bool(settings, "split_file", true); + obs_data_set_int(settings, "max_time_sec", splitFileTime * 60); + obs_data_set_int(settings, "max_size_mb", splitFileSize); + } + + obs_output_update(fileOutput, settings); + } + + if (!obs_output_start(fileOutput)) { + QString error_reason; + const char *error = obs_output_get_last_error(fileOutput); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartRecordingFailed"), error_reason); + return false; + } + + return true; +} + +bool AdvancedOutput::StartReplayBuffer() +{ + const char *path; + const char *recFormat; + const char *filenameFormat; + bool noSpace = false; + bool overwriteIfExists = false; + const char *rbPrefix; + const char *rbSuffix; + int rbTime; + int rbSize; + + if (!useStreamEncoder) { + if (!ffmpegOutput) + UpdateRecordingSettings(); + } else if (!obs_output_active(StreamingOutput())) { + UpdateStreamSettings(); + } + + UpdateAudioSettings(); + + if (!Active()) + SetupOutputs(); + + if (!ffmpegOutput || ffmpegRecording) { + path = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFFilePath" : "RecFilePath"); + recFormat = config_get_string(main->Config(), "AdvOut", ffmpegRecording ? "FFExtension" : "RecFormat2"); + filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + noSpace = config_get_bool(main->Config(), "AdvOut", + ffmpegRecording ? "FFFileNameWithoutSpace" : "RecFileNameWithoutSpace"); + rbPrefix = config_get_string(main->Config(), "SimpleOutput", "RecRBPrefix"); + rbSuffix = config_get_string(main->Config(), "SimpleOutput", "RecRBSuffix"); + rbTime = config_get_int(main->Config(), "AdvOut", "RecRBTime"); + rbSize = config_get_int(main->Config(), "AdvOut", "RecRBSize"); + + string f = GetFormatString(filenameFormat, rbPrefix, rbSuffix); + string ext = GetFormatExt(recFormat); + + OBSDataAutoRelease settings = obs_data_create(); + + obs_data_set_string(settings, "directory", path); + obs_data_set_string(settings, "format", f.c_str()); + obs_data_set_string(settings, "extension", ext.c_str()); + obs_data_set_bool(settings, "allow_spaces", !noSpace); + obs_data_set_int(settings, "max_time_sec", rbTime); + obs_data_set_int(settings, "max_size_mb", usesBitrate ? 0 : rbSize); + + obs_output_update(replayBuffer, settings); + } + + if (!obs_output_start(replayBuffer)) { + QString error_reason; + const char *error = obs_output_get_last_error(replayBuffer); + if (error) + error_reason = QT_UTF8(error); + else + error_reason = QTStr("Output.StartFailedGeneric"); + QMessageBox::critical(main, QTStr("Output.StartReplayFailed"), error_reason); + return false; + } + + return true; +} + +void AdvancedOutput::StopStreaming(bool force) +{ + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); + else + obs_output_stop(output); +} + +void AdvancedOutput::StopRecording(bool force) +{ + if (force) + obs_output_force_stop(fileOutput); + else + obs_output_stop(fileOutput); +} + +void AdvancedOutput::StopReplayBuffer(bool force) +{ + if (force) + obs_output_force_stop(replayBuffer); + else + obs_output_stop(replayBuffer); +} + +bool AdvancedOutput::StreamingActive() const +{ + return obs_output_active(StreamingOutput()); +} + +bool AdvancedOutput::RecordingActive() const +{ + return obs_output_active(fileOutput); +} + +bool AdvancedOutput::ReplayBufferActive() const +{ + return obs_output_active(replayBuffer); +} + +/* ------------------------------------------------------------------------ */ + +void BasicOutputHandler::SetupAutoRemux(const char *&container) +{ + bool autoRemux = config_get_bool(main->Config(), "Video", "AutoRemux"); + if (autoRemux && strcmp(container, "mp4") == 0) + container = "mkv"; +} + +std::string BasicOutputHandler::GetRecordingFilename(const char *path, const char *container, bool noSpace, + bool overwrite, const char *format, bool ffmpeg) +{ + if (!ffmpeg) + SetupAutoRemux(container); + + string dst = GetOutputFilename(path, container, noSpace, overwrite, format); + lastRecordingPath = dst; + return dst; +} + +extern std::string DeserializeConfigText(const char *text); + +std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, std::string audio_encoder_id, + size_t main_audio_mixer, + std::optional vod_track_mixer, + std::function)> continuation) +{ + auto start_streaming_guard = std::make_shared(); + if (!multitrackVideo) { + continuation(std::nullopt); + return start_streaming_guard->GetFuture(); + } + + multitrackVideoActive = false; + + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + bool is_custom = strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; + + std::optional custom_config = std::nullopt; + if (config_get_bool(main->Config(), "Stream1", "MultitrackVideoConfigOverrideEnabled")) + custom_config = DeserializeConfigText( + config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride")); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + QString key = obs_data_get_string(settings, "key"); + + const char *service_name = ""; + if (is_custom && obs_data_has_user_value(settings, "service_name")) { + service_name = obs_data_get_string(settings, "service_name"); + } else if (!is_custom) { + service_name = obs_data_get_string(settings, "service"); + } + + std::optional custom_rtmp_url; + std::optional use_rtmps; + auto server = obs_data_get_string(settings, "server"); + if (strncmp(server, "auto", 4) != 0) { + custom_rtmp_url = server; + } else { + QString server_ = server; + use_rtmps = server_.contains("rtmps", Qt::CaseInsensitive); + } + + auto service_custom_server = obs_data_get_bool(settings, "using_custom_server"); + if (custom_rtmp_url.has_value()) { + blog(LOG_INFO, "Using %sserver '%s'", service_custom_server ? "custom " : "", custom_rtmp_url->c_str()); + } + + auto maximum_aggregate_bitrate = + config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto") + ? std::nullopt + : std::make_optional( + config_get_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate")); + + auto maximum_video_tracks = config_get_bool(main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracksAuto") + ? std::nullopt + : std::make_optional(config_get_int( + main->Config(), "Stream1", "MultitrackVideoMaximumVideoTracks")); + + auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); + + auto continue_on_main_thread = [&, start_streaming_guard, service = OBSService{service}, + continuation = + std::move(continuation)](std::optional error) { + if (error) { + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name"); + } + + multitrackVideoActive = false; + if (!error->ShowDialog(main, multitrack_video_name)) + return continuation(false); + return continuation(std::nullopt); + } + + multitrackVideoActive = true; + + auto signal_handler = multitrackVideo->StreamingSignalHandler(); + + streamDelayStarting.Connect(signal_handler, "starting", OBSStreamStarting, this); + streamStopping.Connect(signal_handler, "stopping", OBSStreamStopping, this); + + startStreaming.Connect(signal_handler, "start", OBSStartStreaming, this); + stopStreaming.Connect(signal_handler, "stop", OBSStopStreaming, this); + return continuation(true); + }; + + QThreadPool::globalInstance()->start([=, multitrackVideo = multitrackVideo.get(), + service_name = std::string{service_name}, service = OBSService{service}, + stream_dump_config = OBSData{stream_dump_config}, + start_streaming_guard = start_streaming_guard]() mutable { + std::optional error; + try { + multitrackVideo->PrepareStreaming(main, service_name.c_str(), service, custom_rtmp_url, key, + audio_encoder_id.c_str(), maximum_aggregate_bitrate, + maximum_video_tracks, custom_config, stream_dump_config, + main_audio_mixer, vod_track_mixer, use_rtmps); + } catch (const MultitrackVideoError &error_) { + error.emplace(error_); + } + + QMetaObject::invokeMethod(main, [=] { continue_on_main_thread(error); }); + }); + + return start_streaming_guard->GetFuture(); +} + +OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() +{ + auto stream_dump_enabled = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); + + if (!stream_dump_enabled) + return nullptr; + + const char *path = config_get_string(main->Config(), "SimpleOutput", "FilePath"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + bool useMP4 = config_get_bool(main->Config(), "Stream1", "MultitrackVideoStreamDumpAsMP4"); + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, useMP4 ? "mp4" : "flv", noSpace, overwriteIfExists, f.c_str(), + // never remux stream dump + false); + obs_data_set_string(settings, "path", strPath.c_str()); + + if (useMP4) { + obs_data_set_bool(settings, "use_mp4", true); + obs_data_set_string(settings, "muxer_settings", "write_encoder_info=1"); + } + + return settings; +} + +BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) +{ + return new SimpleOutput(main); +} + +BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main) +{ + return new AdvancedOutput(main); +} diff --git a/UI/qt-display.cpp b/frontend/utility/SurfaceEventFilter.hpp similarity index 100% rename from UI/qt-display.cpp rename to frontend/utility/SurfaceEventFilter.hpp diff --git a/frontend/utility/VolumeMeterTimer.cpp b/frontend/utility/VolumeMeterTimer.cpp new file mode 100644 index 000000000..cd00c14d7 --- /dev/null +++ b/frontend/utility/VolumeMeterTimer.cpp @@ -0,0 +1,1507 @@ +#include "window-basic-main.hpp" +#include "moc_volume-control.cpp" +#include "obs-app.hpp" +#include "mute-checkbox.hpp" +#include "absolute-slider.hpp" +#include "source-label.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +#define FADER_PRECISION 4096.0 + +// Size of the audio indicator in pixels +#define INDICATOR_THICKNESS 3 + +// Padding on top and bottom of vertical meters +#define METER_PADDING 1 + +std::weak_ptr VolumeMeter::updateTimer; + +static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) +{ + if (muted) + return Qt::Checked; + else if (unassigned) + return Qt::PartiallyChecked; + else + return Qt::Unchecked; +} + +static inline bool IsSourceUnassigned(obs_source_t *source) +{ + uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); + obs_monitoring_type mt = obs_source_get_monitoring_type(source); + + return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; +} + +static void ShowUnassignedWarning(const char *name) +{ + auto msgBox = [=]() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); + msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); +} + +void VolControl::OBSVolumeChanged(void *data, float db) +{ + Q_UNUSED(db); + VolControl *volControl = static_cast(data); + + QMetaObject::invokeMethod(volControl, "VolumeChanged"); +} + +void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + VolControl *volControl = static_cast(data); + + volControl->volMeter->setLevels(magnitude, peak, inputPeak); +} + +void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) +{ + VolControl *volControl = static_cast(data); + bool muted = calldata_bool(calldata, "muted"); + + QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); +} + +void VolControl::VolumeChanged() +{ + slider->blockSignals(true); + slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); + slider->blockSignals(false); + + updateText(); +} + +void VolControl::VolumeMuted(bool muted) +{ + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) +{ + + VolControl *volControl = static_cast(data); + QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); +} + +void VolControl::MixersOrMonitoringChanged() +{ + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::SetMuted(bool) +{ + bool checked = mute->checkState() == Qt::Checked; + bool prev = obs_source_muted(source); + obs_source_set_muted(source, checked); + bool unassigned = IsSourceUnassigned(source); + + if (!checked && unassigned) { + mute->setCheckState(Qt::PartiallyChecked); + /* Show notice about the source no being assigned to any tracks */ + bool has_shown_warning = + config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); + if (!has_shown_warning) + ShowUnassignedWarning(obs_source_get_name(source)); + } + + auto undo_redo = [](const std::string &uuid, bool val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_muted(source, val); + }; + + QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); + + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); +} + +void VolControl::SliderChanged(int vol) +{ + float prev = obs_source_get_volume(source); + + obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); + updateText(); + + auto undo_redo = [](const std::string &uuid, float val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_volume(source, val); + }; + + float val = obs_source_get_volume(source); + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), + std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); +} + +void VolControl::updateText() +{ + QString text; + float db = obs_fader_get_db(obs_fader); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + volLabel->setText(text); + + bool muted = obs_source_muted(source); + const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; + + QString sourceName = obs_source_get_name(source); + QString accText = QTStr(accTextLookup).arg(sourceName); + + slider->setAccessibleName(accText); +} + +void VolControl::EmitConfigClicked() +{ + emit ConfigClicked(); +} + +void VolControl::SetMeterDecayRate(qreal q) +{ + volMeter->setPeakDecayRate(q); +} + +void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + volMeter->setPeakMeterType(peakMeterType); +} + +VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) + : source(std::move(source_)), + levelTotal(0.0f), + levelCount(0.0f), + obs_fader(obs_fader_create(OBS_FADER_LOG)), + obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), + vertical(vertical), + contextMenu(nullptr) +{ + nameLabel = new OBSSourceLabel(source); + volLabel = new QLabel(); + mute = new MuteCheckBox(); + + volLabel->setObjectName("volLabel"); + volLabel->setAlignment(Qt::AlignCenter); + +#ifdef __APPLE__ + mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + QString sourceName = obs_source_get_name(source); + setObjectName(sourceName); + + if (showConfig) { + config = new QPushButton(this); + config->setProperty("class", "icon-dots-vert"); + config->setAutoDefault(false); + + config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); + + connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); + } + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + if (vertical) { + QHBoxLayout *nameLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QHBoxLayout *volLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QHBoxLayout *meterLayout = new QHBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, true); + slider = new VolumeSlider(obs_fader, Qt::Vertical); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + nameLayout->setAlignment(Qt::AlignCenter); + meterLayout->setAlignment(Qt::AlignCenter); + controlLayout->setAlignment(Qt::AlignCenter); + volLayout->setAlignment(Qt::AlignCenter); + + meterFrame->setObjectName("volMeterFrame"); + + nameLayout->setContentsMargins(0, 0, 0, 0); + nameLayout->setSpacing(0); + nameLayout->addWidget(nameLabel); + + controlLayout->setContentsMargins(0, 0, 0, 0); + controlLayout->setSpacing(0); + + // Add Headphone (audio monitoring) widget here + controlLayout->addWidget(mute); + + if (showConfig) { + controlLayout->addWidget(config); + } + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + meterLayout->addWidget(slider); + meterLayout->addWidget(volMeter); + + meterFrame->setLayout(meterLayout); + + volLayout->setContentsMargins(0, 0, 0, 0); + volLayout->setSpacing(0); + volLayout->addWidget(volLabel); + volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); + + mainLayout->addItem(nameLayout); + mainLayout->addItem(volLayout); + mainLayout->addWidget(meterFrame); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + + // Default size can cause clipping of long names in vertical layout. + QFont font = nameLabel->font(); + QFontInfo info(font); + nameLabel->setFont(font); + + setMaximumWidth(110); + } else { + QHBoxLayout *textLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QVBoxLayout *meterLayout = new QVBoxLayout; + QVBoxLayout *buttonLayout = new QVBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, false); + volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + + slider = new VolumeSlider(obs_fader, Qt::Horizontal); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + textLayout->setContentsMargins(0, 0, 0, 0); + textLayout->addWidget(nameLabel); + textLayout->addWidget(volLabel); + textLayout->setAlignment(nameLabel, Qt::AlignLeft); + textLayout->setAlignment(volLabel, Qt::AlignRight); + + meterFrame->setObjectName("volMeterFrame"); + meterFrame->setLayout(meterLayout); + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + + meterLayout->addWidget(volMeter); + meterLayout->addWidget(slider); + + buttonLayout->setContentsMargins(0, 0, 0, 0); + buttonLayout->setSpacing(0); + + if (showConfig) { + buttonLayout->addWidget(config); + } + buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); + buttonLayout->addWidget(mute); + + controlLayout->addItem(buttonLayout); + controlLayout->addWidget(meterFrame); + + mainLayout->addItem(textLayout); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + } + + setLayout(mainLayout); + + nameLabel->setText(sourceName); + + slider->setMinimum(0); + slider->setMaximum(int(FADER_PRECISION)); + + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + mute->setCheckState(GetCheckState(muted, unassigned)); + volMeter->muted = muted || unassigned; + mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); + obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, + this); + + QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); + QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); + + obs_fader_attach_source(obs_fader, source); + obs_volmeter_attach_source(obs_volmeter, source); + + /* Call volume changed once to init the slider position and label */ + VolumeChanged(); +} + +void VolControl::EnableSlider(bool enable) +{ + slider->setEnabled(enable); +} + +VolControl::~VolControl() +{ + obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.clear(); + + if (contextMenu) + contextMenu->close(); +} + +static inline QColor color_from_int(long long val) +{ + QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); + color.setAlpha(255); + + return color; +} + +QColor VolumeMeter::getBackgroundNominalColor() const +{ + return p_backgroundNominalColor; +} + +QColor VolumeMeter::getBackgroundNominalColorDisabled() const +{ + return backgroundNominalColorDisabled; +} + +void VolumeMeter::setBackgroundNominalColor(QColor c) +{ + p_backgroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreen")); + } else { + backgroundNominalColor = p_backgroundNominalColor; + } +} + +void VolumeMeter::setBackgroundNominalColorDisabled(QColor c) +{ + backgroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundWarningColor() const +{ + return p_backgroundWarningColor; +} + +QColor VolumeMeter::getBackgroundWarningColorDisabled() const +{ + return backgroundWarningColorDisabled; +} + +void VolumeMeter::setBackgroundWarningColor(QColor c) +{ + p_backgroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellow")); + } else { + backgroundWarningColor = p_backgroundWarningColor; + } +} + +void VolumeMeter::setBackgroundWarningColorDisabled(QColor c) +{ + backgroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundErrorColor() const +{ + return p_backgroundErrorColor; +} + +QColor VolumeMeter::getBackgroundErrorColorDisabled() const +{ + return backgroundErrorColorDisabled; +} + +void VolumeMeter::setBackgroundErrorColor(QColor c) +{ + p_backgroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRed")); + } else { + backgroundErrorColor = p_backgroundErrorColor; + } +} + +void VolumeMeter::setBackgroundErrorColorDisabled(QColor c) +{ + backgroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundNominalColor() const +{ + return p_foregroundNominalColor; +} + +QColor VolumeMeter::getForegroundNominalColorDisabled() const +{ + return foregroundNominalColorDisabled; +} + +void VolumeMeter::setForegroundNominalColor(QColor c) +{ + p_foregroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreenActive")); + } else { + foregroundNominalColor = p_foregroundNominalColor; + } +} + +void VolumeMeter::setForegroundNominalColorDisabled(QColor c) +{ + foregroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundWarningColor() const +{ + return p_foregroundWarningColor; +} + +QColor VolumeMeter::getForegroundWarningColorDisabled() const +{ + return foregroundWarningColorDisabled; +} + +void VolumeMeter::setForegroundWarningColor(QColor c) +{ + p_foregroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellowActive")); + } else { + foregroundWarningColor = p_foregroundWarningColor; + } +} + +void VolumeMeter::setForegroundWarningColorDisabled(QColor c) +{ + foregroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundErrorColor() const +{ + return p_foregroundErrorColor; +} + +QColor VolumeMeter::getForegroundErrorColorDisabled() const +{ + return foregroundErrorColorDisabled; +} + +void VolumeMeter::setForegroundErrorColor(QColor c) +{ + p_foregroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRedActive")); + } else { + foregroundErrorColor = p_foregroundErrorColor; + } +} + +void VolumeMeter::setForegroundErrorColorDisabled(QColor c) +{ + foregroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getClipColor() const +{ + return clipColor; +} + +void VolumeMeter::setClipColor(QColor c) +{ + clipColor = std::move(c); +} + +QColor VolumeMeter::getMagnitudeColor() const +{ + return magnitudeColor; +} + +void VolumeMeter::setMagnitudeColor(QColor c) +{ + magnitudeColor = std::move(c); +} + +QColor VolumeMeter::getMajorTickColor() const +{ + return majorTickColor; +} + +void VolumeMeter::setMajorTickColor(QColor c) +{ + majorTickColor = std::move(c); +} + +QColor VolumeMeter::getMinorTickColor() const +{ + return minorTickColor; +} + +void VolumeMeter::setMinorTickColor(QColor c) +{ + minorTickColor = std::move(c); +} + +int VolumeMeter::getMeterThickness() const +{ + return meterThickness; +} + +void VolumeMeter::setMeterThickness(int v) +{ + meterThickness = v; + recalculateLayout = true; +} + +qreal VolumeMeter::getMeterFontScaling() const +{ + return meterFontScaling; +} + +void VolumeMeter::setMeterFontScaling(qreal v) +{ + meterFontScaling = v; + recalculateLayout = true; +} + +void VolControl::refreshColors() +{ + volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); + volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); + volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); + volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); + volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); + volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); +} + +qreal VolumeMeter::getMinimumLevel() const +{ + return minimumLevel; +} + +void VolumeMeter::setMinimumLevel(qreal v) +{ + minimumLevel = v; +} + +qreal VolumeMeter::getWarningLevel() const +{ + return warningLevel; +} + +void VolumeMeter::setWarningLevel(qreal v) +{ + warningLevel = v; +} + +qreal VolumeMeter::getErrorLevel() const +{ + return errorLevel; +} + +void VolumeMeter::setErrorLevel(qreal v) +{ + errorLevel = v; +} + +qreal VolumeMeter::getClipLevel() const +{ + return clipLevel; +} + +void VolumeMeter::setClipLevel(qreal v) +{ + clipLevel = v; +} + +qreal VolumeMeter::getMinimumInputLevel() const +{ + return minimumInputLevel; +} + +void VolumeMeter::setMinimumInputLevel(qreal v) +{ + minimumInputLevel = v; +} + +qreal VolumeMeter::getPeakDecayRate() const +{ + return peakDecayRate; +} + +void VolumeMeter::setPeakDecayRate(qreal v) +{ + peakDecayRate = v; +} + +qreal VolumeMeter::getMagnitudeIntegrationTime() const +{ + return magnitudeIntegrationTime; +} + +void VolumeMeter::setMagnitudeIntegrationTime(qreal v) +{ + magnitudeIntegrationTime = v; +} + +qreal VolumeMeter::getPeakHoldDuration() const +{ + return peakHoldDuration; +} + +void VolumeMeter::setPeakHoldDuration(qreal v) +{ + peakHoldDuration = v; +} + +qreal VolumeMeter::getInputPeakHoldDuration() const +{ + return inputPeakHoldDuration; +} + +void VolumeMeter::setInputPeakHoldDuration(qreal v) +{ + inputPeakHoldDuration = v; +} + +void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); + switch (peakMeterType) { + case TRUE_PEAK_METER: + // For true-peak meters EBU has defined the Permitted Maximum, + // taking into account the accuracy of the meter and further + // processing required by lossy audio compression. + // + // The alignment level was not specified, but I've adjusted + // it compared to a sample-peak meter. Incidentally Youtube + // uses this new Alignment Level as the maximum integrated + // loudness of a video. + // + // * Permitted Maximum Level (PML) = -2.0 dBTP + // * Alignment Level (AL) = -13 dBTP + setErrorLevel(-2.0); + setWarningLevel(-13.0); + break; + + case SAMPLE_PEAK_METER: + default: + // For a sample Peak Meter EBU has the following level + // definitions, taking into account inaccuracies of this meter: + // + // * Permitted Maximum Level (PML) = -9.0 dBFS + // * Alignment Level (AL) = -20.0 dBFS + setErrorLevel(-9.0); + setWarningLevel(-20.0); + break; + } +} + +void VolumeMeter::mousePressEvent(QMouseEvent *event) +{ + setFocus(Qt::MouseFocusReason); + event->accept(); +} + +void VolumeMeter::wheelEvent(QWheelEvent *event) +{ + QApplication::sendEvent(focusProxy(), event); +} + +VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, bool vertical) + : QWidget(parent), + obs_volmeter(obs_volmeter), + vertical(vertical) +{ + setAttribute(Qt::WA_OpaquePaintEvent, true); + + // Default meter settings, they only show if + // there is no stylesheet, do not remove. + backgroundNominalColor.setRgb(0x26, 0x7f, 0x26); // Dark green + backgroundWarningColor.setRgb(0x7f, 0x7f, 0x26); // Dark yellow + backgroundErrorColor.setRgb(0x7f, 0x26, 0x26); // Dark red + foregroundNominalColor.setRgb(0x4c, 0xff, 0x4c); // Bright green + foregroundWarningColor.setRgb(0xff, 0xff, 0x4c); // Bright yellow + foregroundErrorColor.setRgb(0xff, 0x4c, 0x4c); // Bright red + + backgroundNominalColorDisabled.setRgb(90, 90, 90); + backgroundWarningColorDisabled.setRgb(117, 117, 117); + backgroundErrorColorDisabled.setRgb(65, 65, 65); + foregroundNominalColorDisabled.setRgb(163, 163, 163); + foregroundWarningColorDisabled.setRgb(217, 217, 217); + foregroundErrorColorDisabled.setRgb(113, 113, 113); + + clipColor.setRgb(0xff, 0xff, 0xff); // Bright white + magnitudeColor.setRgb(0x00, 0x00, 0x00); // Black + majorTickColor.setRgb(0x00, 0x00, 0x00); // Black + minorTickColor.setRgb(0x32, 0x32, 0x32); // Dark gray + minimumLevel = -60.0; // -60 dB + warningLevel = -20.0; // -20 dB + errorLevel = -9.0; // -9 dB + clipLevel = -0.5; // -0.5 dB + minimumInputLevel = -50.0; // -50 dB + peakDecayRate = 11.76; // 20 dB / 1.7 sec + magnitudeIntegrationTime = 0.3; // 99% in 300 ms + peakHoldDuration = 20.0; // 20 seconds + inputPeakHoldDuration = 1.0; // 1 second + meterThickness = 3; // Bar thickness in pixels + meterFontScaling = 0.7; // Font size for numbers is 70% of Widget's font size + channels = (int)audio_output_get_channels(obs_get_audio()); + + doLayout(); + updateTimerRef = updateTimer.lock(); + if (!updateTimerRef) { + updateTimerRef = std::make_shared(); + updateTimerRef->setTimerType(Qt::PreciseTimer); + updateTimerRef->start(16); + updateTimer = updateTimerRef; + } + + updateTimerRef->AddVolControl(this); +} + +VolumeMeter::~VolumeMeter() +{ + updateTimerRef->RemoveVolControl(this); +} + +void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + uint64_t ts = os_gettime_ns(); + QMutexLocker locker(&dataMutex); + + currentLastUpdateTime = ts; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = magnitude[channelNr]; + currentPeak[channelNr] = peak[channelNr]; + currentInputPeak[channelNr] = inputPeak[channelNr]; + } + + // In case there are more updates then redraws we must make sure + // that the ballistics of peak and hold are recalculated. + locker.unlock(); + calculateBallistics(ts); +} + +inline void VolumeMeter::resetLevels() +{ + currentLastUpdateTime = 0; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = -M_INFINITE; + currentPeak[channelNr] = -M_INFINITE; + currentInputPeak[channelNr] = -M_INFINITE; + + displayMagnitude[channelNr] = -M_INFINITE; + displayPeak[channelNr] = -M_INFINITE; + displayPeakHold[channelNr] = -M_INFINITE; + displayPeakHoldLastUpdateTime[channelNr] = 0; + displayInputPeakHold[channelNr] = -M_INFINITE; + displayInputPeakHoldLastUpdateTime[channelNr] = 0; + } +} + +bool VolumeMeter::needLayoutChange() +{ + int currentNrAudioChannels = obs_volmeter_get_nr_channels(obs_volmeter); + + if (!currentNrAudioChannels) { + struct obs_audio_info oai; + obs_get_audio_info(&oai); + currentNrAudioChannels = (oai.speakers == SPEAKERS_MONO) ? 1 : 2; + } + + if (displayNrAudioChannels != currentNrAudioChannels) { + displayNrAudioChannels = currentNrAudioChannels; + recalculateLayout = true; + } + + return recalculateLayout; +} + +// When this is called from the constructor, obs_volmeter_get_nr_channels has not +// yet been called and Q_PROPERTY settings have not yet been read from the +// stylesheet. +inline void VolumeMeter::doLayout() +{ + QMutexLocker locker(&dataMutex); + + if (displayNrAudioChannels) { + int meterSize = std::floor(22 / displayNrAudioChannels); + setMeterThickness(std::clamp(meterSize, 3, 7)); + } + recalculateLayout = false; + + tickFont = font(); + QFontInfo info(tickFont); + tickFont.setPointSizeF(info.pointSizeF() * meterFontScaling); + QFontMetrics metrics(tickFont); + if (vertical) { + // Each meter channel is meterThickness pixels wide, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, space to hold our longest label in this font, + // and a few pixels before the fader. + QRect scaleBounds = metrics.boundingRect("-88"); + setMinimumSize(displayNrAudioChannels * (meterThickness + 1) - 1 + 10 + scaleBounds.width() + 2, 100); + } else { + // Each meter channel is meterThickness pixels high, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, and space high enough to hold our label in + // this font, presuming that digits don't have descenders. + setMinimumSize(100, displayNrAudioChannels * (meterThickness + 1) - 1 + 4 + metrics.capHeight()); + } + + resetLevels(); +} + +inline bool VolumeMeter::detectIdle(uint64_t ts) +{ + double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; + if (timeSinceLastUpdate > 0.5) { + resetLevels(); + return true; + } else { + return false; + } +} + +inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw) +{ + if (currentPeak[channelNr] >= displayPeak[channelNr] || isnan(displayPeak[channelNr])) { + // Attack of peak is immediate. + displayPeak[channelNr] = currentPeak[channelNr]; + } else { + // Decay of peak is 40 dB / 1.7 seconds for Fast Profile + // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) + // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) + float decay = float(peakDecayRate * timeSinceLastRedraw); + displayPeak[channelNr] = + std::clamp(displayPeak[channelNr] - decay, std::min(currentPeak[channelNr], 0.f), 0.f); + } + + if (currentPeak[channelNr] >= displayPeakHold[channelNr] || !isfinite(displayPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak + // after 20 seconds. + qreal timeSinceLastPeak = (uint64_t)(ts - displayPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > peakHoldDuration) { + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (currentInputPeak[channelNr] >= displayInputPeakHold[channelNr] || + !isfinite(displayInputPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak after 1 second. + qreal timeSinceLastPeak = (uint64_t)(ts - displayInputPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > inputPeakHoldDuration) { + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (!isfinite(displayMagnitude[channelNr])) { + // The statements in the else-leg do not work with + // NaN and infinite displayMagnitude. + displayMagnitude[channelNr] = currentMagnitude[channelNr]; + } else { + // A VU meter will integrate to the new value to 99% in 300 ms. + // The calculation here is very simplified and is more accurate + // with higher frame-rate. + float attack = float((currentMagnitude[channelNr] - displayMagnitude[channelNr]) * + (timeSinceLastRedraw / magnitudeIntegrationTime) * 0.99); + displayMagnitude[channelNr] = + std::clamp(displayMagnitude[channelNr] + attack, (float)minimumLevel, 0.f); + } +} + +inline void VolumeMeter::calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw) +{ + QMutexLocker locker(&dataMutex); + + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) + calculateBallisticsForChannel(channelNr, ts, timeSinceLastRedraw); +} + +void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold) +{ + QMutexLocker locker(&dataMutex); + QColor color; + + if (peakHold < minimumInputLevel) + color = backgroundNominalColor; + else if (peakHold < warningLevel) + color = foregroundNominalColor; + else if (peakHold < errorLevel) + color = foregroundWarningColor; + else if (peakHold <= clipLevel) + color = foregroundErrorColor; + else + color = clipColor; + + painter.fillRect(x, y, width, height, color); +} + +void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width) +{ + qreal scale = width / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = int(x + width - (i * scale) - 1); + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + QRect textBounds = metrics.boundingRect(str); + int pos; + if (i == 0) { + pos = position - textBounds.width(); + } else { + pos = position - (textBounds.width() / 2); + if (pos < 0) + pos = 0; + } + painter.drawText(pos, y + 4 + metrics.capHeight(), str); + + painter.drawLine(position, y, position, y + 2); + } +} + +void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) +{ + qreal scale = height / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = y + int(i * scale) + METER_PADDING; + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + if (i == 0) { + painter.drawText(x + 10, position + metrics.capHeight(), str); + } else { + painter.drawText(x + 8, position + (metrics.capHeight() / 2), str); + } + + painter.drawLine(x, position, x + 2, position); + } +} + +#define CLIP_FLASH_DURATION_MS 1000 + +inline int VolumeMeter::convertToInt(float number) +{ + constexpr int min = std::numeric_limits::min(); + constexpr int max = std::numeric_limits::max(); + + // NOTE: Conversion from 'const int' to 'float' changes max value from 2147483647 to 2147483648 + if (number >= (float)max) + return max; + else if (number < min) + return min; + else + return int(number); +} + +void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = width / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = x + 0; + int maximumPosition = x + width; + int magnitudePosition = x + width - convertToInt(magnitude * scale); + int peakPosition = x + width - convertToInt(peak * scale); + int peakHoldPosition = x + width - convertToInt(peakHold * scale); + int warningPosition = x + width - convertToInt(warningLevel * scale); + int errorPosition = x + width - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(minimumPosition, y, peakPosition - minimumPosition, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(peakPosition, y, warningPosition - peakPosition, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, peakPosition - warningPosition, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(peakPosition, y, errorPosition - peakPosition, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(errorPosition, y, peakPosition - errorPosition, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(peakPosition, y, maximumPosition - peakPosition, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(minimumPosition, y, end, height, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(magnitudePosition - 3, y, 3, height, magnitudeColor); +} + +void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = height / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = y + 0; + int maximumPosition = y + height; + int magnitudePosition = y + height - convertToInt(magnitude * scale); + int peakPosition = y + height - convertToInt(peak * scale); + int peakHoldPosition = y + height - convertToInt(peakHold * scale); + int warningPosition = y + height - convertToInt(warningLevel * scale); + int errorPosition = y + height - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(x, minimumPosition, width, peakPosition - minimumPosition, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, peakPosition, width, warningPosition - peakPosition, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, peakPosition - warningPosition, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, peakPosition, width, errorPosition - peakPosition, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, errorPosition, width, peakPosition - errorPosition, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(x, peakPosition, width, maximumPosition - peakPosition, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(x, minimumPosition, width, end, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(x, magnitudePosition - 3, width, 3, magnitudeColor); +} + +void VolumeMeter::paintEvent(QPaintEvent *event) +{ + uint64_t ts = os_gettime_ns(); + qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; + calculateBallistics(ts, timeSinceLastRedraw); + bool idle = detectIdle(ts); + + QRect widgetRect = rect(); + int width = widgetRect.width(); + int height = widgetRect.height(); + + QPainter painter(this); + + // Paint window background color (as widget is opaque) + QColor background = palette().color(QPalette::ColorRole::Window); + painter.fillRect(event->region().boundingRect(), background); + + if (vertical) + height -= METER_PADDING * 2; + + // timerEvent requests update of the bar(s) only, so we can avoid the + // overhead of repainting the scale and labels. + if (event->region().boundingRect() != getBarRect()) { + if (needLayoutChange()) + doLayout(); + + if (vertical) { + paintVTicks(painter, displayNrAudioChannels * (meterThickness + 1) - 1, 0, + height - (INDICATOR_THICKNESS + 3)); + } else { + paintHTicks(painter, INDICATOR_THICKNESS + 3, displayNrAudioChannels * (meterThickness + 1) - 1, + width - (INDICATOR_THICKNESS + 3)); + } + } + + if (vertical) { + // Invert the Y axis to ease the math + painter.translate(0, height + METER_PADDING); + painter.scale(1, -1); + } + + for (int channelNr = 0; channelNr < displayNrAudioChannels; channelNr++) { + + int channelNrFixed = (displayNrAudioChannels == 1 && channels > 2) ? 2 : channelNr; + + if (vertical) + paintVMeter(painter, channelNr * (meterThickness + 1), INDICATOR_THICKNESS + 2, meterThickness, + height - (INDICATOR_THICKNESS + 2), displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + else + paintHMeter(painter, INDICATOR_THICKNESS + 2, channelNr * (meterThickness + 1), + width - (INDICATOR_THICKNESS + 2), meterThickness, displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + + if (idle) + continue; + + // By not drawing the input meter boxes the user can + // see that the audio stream has been stopped, without + // having too much visual impact. + if (vertical) + paintInputMeter(painter, channelNr * (meterThickness + 1), 0, meterThickness, + INDICATOR_THICKNESS, displayInputPeakHold[channelNrFixed]); + else + paintInputMeter(painter, 0, channelNr * (meterThickness + 1), INDICATOR_THICKNESS, + meterThickness, displayInputPeakHold[channelNrFixed]); + } + + lastRedrawTime = ts; +} + +QRect VolumeMeter::getBarRect() const +{ + QRect rec = rect(); + if (vertical) + rec.setWidth(displayNrAudioChannels * (meterThickness + 1) - 1); + else + rec.setHeight(displayNrAudioChannels * (meterThickness + 1) - 1); + + return rec; +} + +void VolumeMeter::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::StyleChange) + recalculateLayout = true; + + QWidget::changeEvent(e); +} + +void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) +{ + volumeMeters.push_back(meter); +} + +void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) +{ + volumeMeters.removeOne(meter); +} + +void VolumeMeterTimer::timerEvent(QTimerEvent *) +{ + for (VolumeMeter *meter : volumeMeters) { + if (meter->needLayoutChange()) { + // Tell paintEvent to update layout and paint everything + meter->update(); + } else { + // Tell paintEvent to paint only the bars + meter->update(meter->getBarRect()); + } + } +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) +{ + fad = fader; +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) + : AbsoluteSlider(orientation, parent) +{ + fad = fader; +} + +bool VolumeSlider::getDisplayTicks() const +{ + return displayTicks; +} + +void VolumeSlider::setDisplayTicks(bool display) +{ + displayTicks = display; +} + +void VolumeSlider::paintEvent(QPaintEvent *event) +{ + if (!getDisplayTicks()) { + QSlider::paintEvent(event); + return; + } + + QPainter painter(this); + QColor tickColor(91, 98, 115, 255); + + obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); + + QStyleOptionSlider opt; + initStyleOption(&opt); + + QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); + QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); + + if (orientation() == Qt::Horizontal) { + const int sliderWidth = groove.width() - handle.width(); + + float tickLength = groove.height() * 1.5; + tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); + + float yPos = groove.center().y() - (tickLength / 2) + 1; + + for (int db = -10; db >= -90; db -= 10) { + float tickValue = fader_db_to_def(db); + + float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); + painter.fillRect(xPos, yPos, 1, tickLength, tickColor); + } + } + + if (orientation() == Qt::Vertical) { + const int sliderHeight = groove.height() - handle.height(); + + float tickLength = groove.width() * 1.5; + tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); + + float xPos = groove.center().x() - (tickLength / 2) + 1; + + for (int db = -10; db >= -96; db -= 10) { + float tickValue = fader_db_to_def(db); + + float yPos = + groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); + painter.fillRect(xPos, yPos, tickLength, 1, tickColor); + } + } + + QSlider::paintEvent(event); +} + +VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} + +VolumeSlider *VolumeAccessibleInterface::slider() const +{ + return qobject_cast(object()); +} + +QString VolumeAccessibleInterface::text(QAccessible::Text t) const +{ + if (slider()->isVisible()) { + switch (t) { + case QAccessible::Text::Value: + return currentValue().toString(); + default: + break; + } + } + return QAccessibleWidget::text(t); +} + +QVariant VolumeAccessibleInterface::currentValue() const +{ + QString text; + float db = obs_fader_get_db(slider()->fad); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + return text; +} + +void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) +{ + slider()->setValue(value.toInt()); +} + +QVariant VolumeAccessibleInterface::maximumValue() const +{ + return slider()->maximum(); +} + +QVariant VolumeAccessibleInterface::minimumValue() const +{ + return slider()->minimum(); +} + +QVariant VolumeAccessibleInterface::minimumStepSize() const +{ + return slider()->singleStep(); +} + +QAccessible::Role VolumeAccessibleInterface::role() const +{ + return QAccessible::Role::Slider; +} diff --git a/frontend/utility/VolumeMeterTimer.hpp b/frontend/utility/VolumeMeterTimer.hpp new file mode 100644 index 000000000..51c77f247 --- /dev/null +++ b/frontend/utility/VolumeMeterTimer.hpp @@ -0,0 +1,341 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "absolute-slider.hpp" + +class QPushButton; +class VolumeMeterTimer; +class VolumeSlider; + +class VolumeMeter : public QWidget { + Q_OBJECT + Q_PROPERTY(QColor backgroundNominalColor READ getBackgroundNominalColor WRITE setBackgroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColor READ getBackgroundWarningColor WRITE setBackgroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor backgroundErrorColor READ getBackgroundErrorColor WRITE setBackgroundErrorColor DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColor READ getForegroundNominalColor WRITE setForegroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColor READ getForegroundWarningColor WRITE setForegroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor foregroundErrorColor READ getForegroundErrorColor WRITE setForegroundErrorColor DESIGNABLE true) + + Q_PROPERTY(QColor backgroundNominalColorDisabled READ getBackgroundNominalColorDisabled WRITE + setBackgroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColorDisabled READ getBackgroundWarningColorDisabled WRITE + setBackgroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundErrorColorDisabled READ getBackgroundErrorColorDisabled WRITE + setBackgroundErrorColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColorDisabled READ getForegroundNominalColorDisabled WRITE + setForegroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColorDisabled READ getForegroundWarningColorDisabled WRITE + setForegroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundErrorColorDisabled READ getForegroundErrorColorDisabled WRITE + setForegroundErrorColorDisabled DESIGNABLE true) + + Q_PROPERTY(QColor clipColor READ getClipColor WRITE setClipColor DESIGNABLE true) + Q_PROPERTY(QColor magnitudeColor READ getMagnitudeColor WRITE setMagnitudeColor DESIGNABLE true) + Q_PROPERTY(QColor majorTickColor READ getMajorTickColor WRITE setMajorTickColor DESIGNABLE true) + Q_PROPERTY(QColor minorTickColor READ getMinorTickColor WRITE setMinorTickColor DESIGNABLE true) + Q_PROPERTY(int meterThickness READ getMeterThickness WRITE setMeterThickness DESIGNABLE true) + Q_PROPERTY(qreal meterFontScaling READ getMeterFontScaling WRITE setMeterFontScaling DESIGNABLE true) + + // Levels are denoted in dBFS. + Q_PROPERTY(qreal minimumLevel READ getMinimumLevel WRITE setMinimumLevel DESIGNABLE true) + Q_PROPERTY(qreal warningLevel READ getWarningLevel WRITE setWarningLevel DESIGNABLE true) + Q_PROPERTY(qreal errorLevel READ getErrorLevel WRITE setErrorLevel DESIGNABLE true) + Q_PROPERTY(qreal clipLevel READ getClipLevel WRITE setClipLevel DESIGNABLE true) + Q_PROPERTY(qreal minimumInputLevel READ getMinimumInputLevel WRITE setMinimumInputLevel DESIGNABLE true) + + // Rates are denoted in dB/second. + Q_PROPERTY(qreal peakDecayRate READ getPeakDecayRate WRITE setPeakDecayRate DESIGNABLE true) + + // Time in seconds for the VU meter to integrate over. + Q_PROPERTY(qreal magnitudeIntegrationTime READ getMagnitudeIntegrationTime WRITE setMagnitudeIntegrationTime + DESIGNABLE true) + + // Duration is denoted in seconds. + Q_PROPERTY(qreal peakHoldDuration READ getPeakHoldDuration WRITE setPeakHoldDuration DESIGNABLE true) + Q_PROPERTY(qreal inputPeakHoldDuration READ getInputPeakHoldDuration WRITE setInputPeakHoldDuration + DESIGNABLE true) + + friend class VolControl; + +private: + obs_volmeter_t *obs_volmeter; + static std::weak_ptr updateTimer; + std::shared_ptr updateTimerRef; + + inline void resetLevels(); + inline void doLayout(); + inline bool detectIdle(uint64_t ts); + inline void calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw = 0.0); + inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw); + + inline int convertToInt(float number); + void paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold); + void paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintHTicks(QPainter &painter, int x, int y, int width); + void paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintVTicks(QPainter &painter, int x, int y, int height); + + QMutex dataMutex; + + bool recalculateLayout = true; + uint64_t currentLastUpdateTime = 0; + float currentMagnitude[MAX_AUDIO_CHANNELS]; + float currentPeak[MAX_AUDIO_CHANNELS]; + float currentInputPeak[MAX_AUDIO_CHANNELS]; + + int displayNrAudioChannels = 0; + float displayMagnitude[MAX_AUDIO_CHANNELS]; + float displayPeak[MAX_AUDIO_CHANNELS]; + float displayPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + float displayInputPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayInputPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + + QFont tickFont; + QColor backgroundNominalColor; + QColor backgroundWarningColor; + QColor backgroundErrorColor; + QColor foregroundNominalColor; + QColor foregroundWarningColor; + QColor foregroundErrorColor; + + QColor backgroundNominalColorDisabled; + QColor backgroundWarningColorDisabled; + QColor backgroundErrorColorDisabled; + QColor foregroundNominalColorDisabled; + QColor foregroundWarningColorDisabled; + QColor foregroundErrorColorDisabled; + + QColor clipColor; + QColor magnitudeColor; + QColor majorTickColor; + QColor minorTickColor; + + int meterThickness; + qreal meterFontScaling; + + qreal minimumLevel; + qreal warningLevel; + qreal errorLevel; + qreal clipLevel; + qreal minimumInputLevel; + qreal peakDecayRate; + qreal magnitudeIntegrationTime; + qreal peakHoldDuration; + qreal inputPeakHoldDuration; + + QColor p_backgroundNominalColor; + QColor p_backgroundWarningColor; + QColor p_backgroundErrorColor; + QColor p_foregroundNominalColor; + QColor p_foregroundWarningColor; + QColor p_foregroundErrorColor; + + uint64_t lastRedrawTime = 0; + int channels = 0; + bool clipping = false; + bool vertical; + bool muted = false; + +public: + explicit VolumeMeter(QWidget *parent = nullptr, obs_volmeter_t *obs_volmeter = nullptr, bool vertical = false); + ~VolumeMeter(); + + void setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]); + QRect getBarRect() const; + bool needLayoutChange(); + + QColor getBackgroundNominalColor() const; + void setBackgroundNominalColor(QColor c); + QColor getBackgroundWarningColor() const; + void setBackgroundWarningColor(QColor c); + QColor getBackgroundErrorColor() const; + void setBackgroundErrorColor(QColor c); + QColor getForegroundNominalColor() const; + void setForegroundNominalColor(QColor c); + QColor getForegroundWarningColor() const; + void setForegroundWarningColor(QColor c); + QColor getForegroundErrorColor() const; + void setForegroundErrorColor(QColor c); + + QColor getBackgroundNominalColorDisabled() const; + void setBackgroundNominalColorDisabled(QColor c); + QColor getBackgroundWarningColorDisabled() const; + void setBackgroundWarningColorDisabled(QColor c); + QColor getBackgroundErrorColorDisabled() const; + void setBackgroundErrorColorDisabled(QColor c); + QColor getForegroundNominalColorDisabled() const; + void setForegroundNominalColorDisabled(QColor c); + QColor getForegroundWarningColorDisabled() const; + void setForegroundWarningColorDisabled(QColor c); + QColor getForegroundErrorColorDisabled() const; + void setForegroundErrorColorDisabled(QColor c); + + QColor getClipColor() const; + void setClipColor(QColor c); + QColor getMagnitudeColor() const; + void setMagnitudeColor(QColor c); + QColor getMajorTickColor() const; + void setMajorTickColor(QColor c); + QColor getMinorTickColor() const; + void setMinorTickColor(QColor c); + int getMeterThickness() const; + void setMeterThickness(int v); + qreal getMeterFontScaling() const; + void setMeterFontScaling(qreal v); + qreal getMinimumLevel() const; + void setMinimumLevel(qreal v); + qreal getWarningLevel() const; + void setWarningLevel(qreal v); + qreal getErrorLevel() const; + void setErrorLevel(qreal v); + qreal getClipLevel() const; + void setClipLevel(qreal v); + qreal getMinimumInputLevel() const; + void setMinimumInputLevel(qreal v); + qreal getPeakDecayRate() const; + void setPeakDecayRate(qreal v); + qreal getMagnitudeIntegrationTime() const; + void setMagnitudeIntegrationTime(qreal v); + qreal getPeakHoldDuration() const; + void setPeakHoldDuration(qreal v); + qreal getInputPeakHoldDuration() const; + void setInputPeakHoldDuration(qreal v); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void wheelEvent(QWheelEvent *event) override; + +protected: + void paintEvent(QPaintEvent *event) override; + void changeEvent(QEvent *e) override; +}; + +class VolumeMeterTimer : public QTimer { + Q_OBJECT + +public: + inline VolumeMeterTimer() : QTimer() {} + + void AddVolControl(VolumeMeter *meter); + void RemoveVolControl(VolumeMeter *meter); + +protected: + void timerEvent(QTimerEvent *event) override; + QList volumeMeters; +}; + +class QLabel; +class VolumeSlider; +class MuteCheckBox; +class OBSSourceLabel; + +class VolControl : public QFrame { + Q_OBJECT + +private: + OBSSource source; + std::vector sigs; + OBSSourceLabel *nameLabel; + QLabel *volLabel; + VolumeMeter *volMeter; + VolumeSlider *slider; + MuteCheckBox *mute; + QPushButton *config = nullptr; + float levelTotal; + float levelCount; + OBSFader obs_fader; + OBSVolMeter obs_volmeter; + bool vertical; + QMenu *contextMenu; + + static void OBSVolumeChanged(void *param, float db); + static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); + static void OBSVolumeMuted(void *data, calldata_t *calldata); + static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); + + void EmitConfigClicked(); + +private slots: + void VolumeChanged(); + void VolumeMuted(bool muted); + void MixersOrMonitoringChanged(); + + void SetMuted(bool checked); + void SliderChanged(int vol); + void updateText(); + +signals: + void ConfigClicked(); + +public: + explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); + ~VolControl(); + + inline obs_source_t *GetSource() const { return source; } + + void SetMeterDecayRate(qreal q); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + + void EnableSlider(bool enable); + inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } + + void refreshColors(); +}; + +class VolumeSlider : public AbsoluteSlider { + Q_OBJECT + +public: + obs_fader_t *fad; + + VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); + VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); + + bool getDisplayTicks() const; + void setDisplayTicks(bool display); + +private: + bool displayTicks = false; + QColor tickColor; + +protected: + virtual void paintEvent(QPaintEvent *event) override; +}; + +class VolumeAccessibleInterface : public QAccessibleWidget { + +public: + VolumeAccessibleInterface(QWidget *w); + + QVariant currentValue() const; + void setCurrentValue(const QVariant &value); + + QVariant maximumValue() const; + QVariant minimumValue() const; + + QVariant minimumStepSize() const; + +private: + VolumeSlider *slider() const; + +protected: + virtual QAccessible::Role role() const override; + virtual QString text(QAccessible::Text t) const override; +}; diff --git a/frontend/widgets/ColorSelect.cpp b/frontend/widgets/ColorSelect.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/ColorSelect.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/ColorSelect.hpp b/frontend/widgets/ColorSelect.hpp new file mode 100644 index 000000000..81bb7b478 --- /dev/null +++ b/frontend/widgets/ColorSelect.hpp @@ -0,0 +1,1382 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "window-main.hpp" +#include "window-basic-interaction.hpp" +#include "window-basic-vcam.hpp" +#include "window-basic-properties.hpp" +#include "window-basic-transform.hpp" +#include "window-basic-adv-audio.hpp" +#include "window-basic-filters.hpp" +#include "window-missing-files.hpp" +#include "window-projector.hpp" +#include "window-basic-about.hpp" +#ifdef YOUTUBE_ENABLED +#include "window-dock-youtube-app.hpp" +#endif +#include "auth-base.hpp" +#include "log-viewer.hpp" +#include "undo-stack-obs.hpp" + +#include + +#include +#include +#include + +#include + +class QMessageBox; +class QListWidgetItem; +class VolControl; +class OBSBasicStats; +class OBSBasicVCamConfig; + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#define DESKTOP_AUDIO_1 Str("DesktopAudioDevice1") +#define DESKTOP_AUDIO_2 Str("DesktopAudioDevice2") +#define AUX_AUDIO_1 Str("AuxAudioDevice1") +#define AUX_AUDIO_2 Str("AuxAudioDevice2") +#define AUX_AUDIO_3 Str("AuxAudioDevice3") +#define AUX_AUDIO_4 Str("AuxAudioDevice4") + +#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" + +#define PREVIEW_EDGE_SIZE 10 + +struct BasicOutputHandler; + +enum class QtDataRole { + OBSRef = Qt::UserRole, + OBSSignals, +}; + +struct SavedProjectorInfo { + ProjectorType type; + int monitor; + std::string geometry; + std::string name; + bool alwaysOnTop; + bool alwaysOnTopOverridden; +}; + +struct SourceCopyInfo { + OBSWeakSource weak_source; + bool visible; + obs_sceneitem_crop crop; + obs_transform_info transform; + obs_blending_method blend_method; + obs_blending_type blend_mode; +}; + +struct QuickTransition { + QPushButton *button = nullptr; + OBSSource source; + obs_hotkey_id hotkey = OBS_INVALID_HOTKEY_ID; + int duration = 0; + int id = 0; + bool fadeToBlack = false; + + inline QuickTransition() {} + inline QuickTransition(OBSSource source_, int duration_, int id_, bool fadeToBlack_ = false) + : source(source_), + duration(duration_), + id(id_), + fadeToBlack(fadeToBlack_), + renamedSignal(std::make_shared(obs_source_get_signal_handler(source), "rename", + SourceRenamed, this)) + { + } + +private: + static void SourceRenamed(void *param, calldata_t *data); + std::shared_ptr renamedSignal; +}; + +struct OBSProfile { + std::string name; + std::string directoryName; + std::filesystem::path path; + std::filesystem::path profileFile; +}; + +struct OBSSceneCollection { + std::string name; + std::string fileName; + std::filesystem::path collectionFile; +}; + +struct OBSPromptResult { + bool success; + std::string promptValue; + bool optionValue; +}; + +struct OBSPromptRequest { + std::string title; + std::string prompt; + std::string promptValue; + bool withOption; + std::string optionPrompt; + bool optionValue; +}; + +using OBSPromptCallback = std::function; + +using OBSProfileCache = std::map; +using OBSSceneCollectionCache = std::map; + +class ColorSelect : public QWidget { + +public: + explicit ColorSelect(QWidget *parent = 0); + +private: + std::unique_ptr ui; +}; + +class OBSBasic : public OBSMainWindow { + Q_OBJECT + Q_PROPERTY(QIcon imageIcon READ GetImageIcon WRITE SetImageIcon DESIGNABLE true) + Q_PROPERTY(QIcon colorIcon READ GetColorIcon WRITE SetColorIcon DESIGNABLE true) + Q_PROPERTY(QIcon slideshowIcon READ GetSlideshowIcon WRITE SetSlideshowIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioInputIcon READ GetAudioInputIcon WRITE SetAudioInputIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioOutputIcon READ GetAudioOutputIcon WRITE SetAudioOutputIcon DESIGNABLE true) + Q_PROPERTY(QIcon desktopCapIcon READ GetDesktopCapIcon WRITE SetDesktopCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon windowCapIcon READ GetWindowCapIcon WRITE SetWindowCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon gameCapIcon READ GetGameCapIcon WRITE SetGameCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon cameraIcon READ GetCameraIcon WRITE SetCameraIcon DESIGNABLE true) + Q_PROPERTY(QIcon textIcon READ GetTextIcon WRITE SetTextIcon DESIGNABLE true) + Q_PROPERTY(QIcon mediaIcon READ GetMediaIcon WRITE SetMediaIcon DESIGNABLE true) + Q_PROPERTY(QIcon browserIcon READ GetBrowserIcon WRITE SetBrowserIcon DESIGNABLE true) + Q_PROPERTY(QIcon groupIcon READ GetGroupIcon WRITE SetGroupIcon DESIGNABLE true) + Q_PROPERTY(QIcon sceneIcon READ GetSceneIcon WRITE SetSceneIcon DESIGNABLE true) + Q_PROPERTY(QIcon defaultIcon READ GetDefaultIcon WRITE SetDefaultIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioProcessOutputIcon READ GetAudioProcessOutputIcon WRITE SetAudioProcessOutputIcon + DESIGNABLE true) + + friend class OBSAbout; + friend class OBSBasicPreview; + friend class OBSBasicStatusBar; + friend class OBSBasicSourceSelect; + friend class OBSBasicTransform; + friend class OBSBasicSettings; + friend class Auth; + friend class AutoConfig; + friend class AutoConfigStreamPage; + friend class RecordButton; + friend class ControlsSplitButton; + friend class ExtraBrowsersModel; + friend class ExtraBrowsersDelegate; + friend class DeviceCaptureToolbar; + friend class OBSBasicSourceSelect; + friend class OBSYoutubeActions; + friend class OBSPermissions; + friend struct BasicOutputHandler; + friend struct OBSStudioAPI; + friend class ScreenshotObj; + + enum class MoveDir { Up, Down, Left, Right }; + + enum DropType { + DropType_RawText, + DropType_Text, + DropType_Image, + DropType_Media, + DropType_Html, + DropType_Url, + }; + + enum ContextBarSize { ContextBarSize_Minimized, ContextBarSize_Reduced, ContextBarSize_Normal }; + + enum class CenterType { + Scene, + Vertical, + Horizontal, + }; + +private: + obs_frontend_callbacks *api = nullptr; + + std::shared_ptr auth; + + std::vector volumes; + + std::vector signalHandlers; + + QList> oldExtraDocks; + QStringList oldExtraDockNames; + + OBSDataAutoRelease collectionModuleData; + std::vector safeModeTransitions; + + bool loaded = false; + long disableSaving = 1; + bool projectChanged = false; + bool previewEnabled = true; + ContextBarSize contextBarSize = ContextBarSize_Normal; + + std::deque clipboard; + OBSWeakSourceAutoRelease copyFiltersSource; + bool copyVisible = true; + obs_transform_info copiedTransformInfo; + obs_sceneitem_crop copiedCropInfo; + bool hasCopiedTransform = false; + OBSWeakSourceAutoRelease copySourceTransition; + int copySourceTransitionDuration; + + bool closing = false; + bool clearingFailed = false; + + QScopedPointer devicePropertiesThread; + QScopedPointer whatsNewInitThread; + QScopedPointer updateCheckThread; + QScopedPointer introCheckThread; + QScopedPointer logUploadThread; + + QPointer interaction; + QPointer properties; + QPointer transformWindow; + QPointer advAudioWindow; + QPointer filters; + QPointer statsDock; +#ifdef YOUTUBE_ENABLED + QPointer youtubeAppDock; + uint64_t lastYouTubeAppDockCreationTime = 0; +#endif + QPointer about; + QPointer missDialog; + QPointer logView; + + QPointer cpuUsageTimer; + QPointer diskFullTimer; + + QPointer nudge_timer; + bool recent_nudge = false; + + os_cpu_usage_info_t *cpuUsageInfo = nullptr; + + OBSService service; + std::unique_ptr outputHandler; + std::shared_future setupStreamingGuard; + bool streamingStopping = false; + bool recordingStopping = false; + bool replayBufferStopping = false; + + gs_vertbuffer_t *box = nullptr; + gs_vertbuffer_t *boxLeft = nullptr; + gs_vertbuffer_t *boxTop = nullptr; + gs_vertbuffer_t *boxRight = nullptr; + gs_vertbuffer_t *boxBottom = nullptr; + gs_vertbuffer_t *circle = nullptr; + + gs_vertbuffer_t *actionSafeMargin = nullptr; + gs_vertbuffer_t *graphicsSafeMargin = nullptr; + gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; + gs_vertbuffer_t *leftLine = nullptr; + gs_vertbuffer_t *topLine = nullptr; + gs_vertbuffer_t *rightLine = nullptr; + + int previewX = 0, previewY = 0; + int previewCX = 0, previewCY = 0; + float previewScale = 0.0f; + + ConfigFile activeConfiguration; + + std::vector savedProjectorsArray; + std::vector projectors; + + QPointer stats; + QPointer remux; + QPointer extraBrowsers; + QPointer importer; + + QPointer transitionButton; + + bool vcamEnabled = false; + VCamConfig vcamConfig; + + QScopedPointer trayIcon; + QPointer sysTrayStream; + QPointer sysTrayRecord; + QPointer sysTrayReplayBuffer; + QPointer sysTrayVirtualCam; + QPointer showHide; + QPointer exit; + QPointer trayMenu; + QPointer previewProjector; + QPointer studioProgramProjector; + QPointer previewProjectorSource; + QPointer previewProjectorMain; + QPointer sceneProjectorMenu; + QPointer sourceProjector; + QPointer scaleFilteringMenu; + QPointer blendingMethodMenu; + QPointer blendingModeMenu; + QPointer colorMenu; + QPointer colorWidgetAction; + QPointer colorSelect; + QPointer deinterlaceMenu; + QPointer perSceneTransitionMenu; + QPointer shortcutFilter; + QPointer renameScene; + QPointer renameSource; + + QPointer programWidget; + QPointer programLayout; + QPointer programLabel; + + QScopedPointer patronJsonThread; + std::string patronJson; + + std::atomic currentScene = nullptr; + std::optional> lastOutputResolution; + std::optional> migrationBaseResolution; + bool usingAbsoluteCoordinates = false; + + void DisableRelativeCoordinates(bool disable); + + void OnEvent(enum obs_frontend_event event); + + void UpdateMultiviewProjectorMenu(); + + void DrawBackdrop(float cx, float cy); + + void SetupEncoders(); + + void CreateFirstRunSources(); + void CreateDefaultScene(bool firstStart); + + void UpdateVolumeControlsDecayRate(); + void UpdateVolumeControlsPeakMeterType(); + void ClearVolumeControls(); + + void UploadLog(const char *subdir, const char *file, const bool crash); + + void Save(const char *file); + void LoadData(obs_data_t *data, const char *file, bool remigrate = false); + void Load(const char *file, bool remigrate = false); + + void InitHotkeys(); + void CreateHotkeys(); + void ClearHotkeys(); + + bool InitService(); + + bool InitBasicConfigDefaults(); + void InitBasicConfigDefaults2(); + bool InitBasicConfig(); + + void InitOBSCallbacks(); + + void InitPrimitives(); + + void OnFirstLoad(); + + OBSSceneItem GetSceneItem(QListWidgetItem *item); + OBSSceneItem GetCurrentSceneItem(); + + bool QueryRemoveSource(obs_source_t *source); + + void TimedCheckForUpdates(); + void CheckForUpdates(bool manualUpdate); + + void GetFPSCommon(uint32_t &num, uint32_t &den) const; + void GetFPSInteger(uint32_t &num, uint32_t &den) const; + void GetFPSFraction(uint32_t &num, uint32_t &den) const; + void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const; + void GetConfigFPS(uint32_t &num, uint32_t &den) const; + + void UpdatePreviewScalingMenu(); + + void LoadSceneListOrder(obs_data_array_t *array); + obs_data_array_t *SaveSceneListOrder(); + void ChangeSceneIndex(bool relative, int idx, int invalidIdx); + + void TempFileOutput(const char *path, int vBitrate, int aBitrate); + void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); + + void CloseDialogs(); + void ClearSceneData(); + void ClearProjectors(); + + void Nudge(int dist, MoveDir dir); + + OBSProjector *OpenProjector(obs_source_t *source, int monitor, ProjectorType type); + + void GetAudioSourceFilters(); + void GetAudioSourceProperties(); + void VolControlContextMenu(); + void ToggleVolControlLayout(); + void ToggleMixerLayout(bool vertical); + + void LogScenes(); + void SaveProjectNow(); + + int GetTopSelectedSourceItem(); + + QModelIndexList GetAllSelectedSourceItems(); + + obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, replayBufHotkeys, vcamHotkeys, + togglePreviewHotkeys, contextBarHotkeys; + obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey, addChapterHotkey; + + void InitDefaultTransitions(); + void InitTransition(obs_source_t *transition); + obs_source_t *FindTransition(const char *name); + OBSSource GetCurrentTransition(); + obs_data_array_t *SaveTransitions(); + void LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data); + + obs_source_t *fadeTransition; + obs_source_t *cutTransition; + + void CreateProgramDisplay(); + void CreateProgramOptions(); + void AddQuickTransitionId(int id); + void AddQuickTransition(); + void AddQuickTransitionHotkey(QuickTransition *qt); + void RemoveQuickTransitionHotkey(QuickTransition *qt); + void LoadQuickTransitions(obs_data_array_t *array); + obs_data_array_t *SaveQuickTransitions(); + void ClearQuickTransitionWidgets(); + void RefreshQuickTransitions(); + void DisableQuickTransitionWidgets(); + void EnableTransitionWidgets(bool enable); + void CreateDefaultQuickTransitions(); + + void PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration); + QMenu *CreatePerSceneTransitionMenu(); + QMenu *CreateVisibilityTransitionMenu(bool visible); + + QuickTransition *GetQuickTransition(int id); + int GetQuickTransitionIdx(int id); + QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt); + void ClearQuickTransitions(); + void QuickTransitionClicked(); + void QuickTransitionChange(); + void QuickTransitionChangeDuration(int value); + void QuickTransitionRemoveClicked(); + + void SetPreviewProgramMode(bool enabled); + void ResizeProgram(uint32_t cx, uint32_t cy); + void SetCurrentScene(obs_scene_t *scene, bool force = false); + static void RenderProgram(void *data, uint32_t cx, uint32_t cy); + + std::vector quickTransitions; + QPointer programOptions; + QPointer program; + OBSWeakSource lastScene; + OBSWeakSource swapScene; + OBSWeakSource programScene; + OBSWeakSource lastProgramScene; + bool editPropertiesMode = false; + bool sceneDuplicationMode = true; + bool swapScenesMode = true; + volatile bool previewProgramMode = false; + obs_hotkey_pair_id togglePreviewProgramHotkeys = 0; + obs_hotkey_id transitionHotkey = 0; + obs_hotkey_id statsHotkey = 0; + obs_hotkey_id screenshotHotkey = 0; + obs_hotkey_id sourceScreenshotHotkey = 0; + int quickTransitionIdCounter = 1; + bool overridingTransition = false; + + int programX = 0, programY = 0; + int programCX = 0, programCY = 0; + float programScale = 0.0f; + + int disableOutputsRef = 0; + + inline void OnActivate(bool force = false); + inline void OnDeactivate(); + + void AddDropSource(const char *file, DropType image); + void AddDropURL(const char *url, QString &name, obs_data_t *settings, const obs_video_info &ovi); + void ConfirmDropUrl(const QString &url); + void dragEnterEvent(QDragEnterEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; + + bool sysTrayMinimizeToTray(); + + void EnumDialogs(); + + QList visDialogs; + QList modalDialogs; + QList visMsgBoxes; + + QList visDlgPositions; + + QByteArray startingDockLayout; + + obs_data_array_t *SaveProjectors(); + void LoadSavedProjectors(obs_data_array_t *savedProjectors); + + void MacBranchesFetched(const QString &branch, bool manualUpdate); + void ReceivedIntroJson(const QString &text); + void ShowWhatsNew(const QString &url); + + void UpdatePreviewProgramIndicators(); + + QStringList extraDockNames; + QList> extraDocks; + + QStringList extraCustomDockNames; + QList> extraCustomDocks; + +#ifdef BROWSER_AVAILABLE + QPointer extraBrowserMenuDocksSeparator; + + QList> extraBrowserDocks; + QStringList extraBrowserDockNames; + QStringList extraBrowserDockTargets; + + void ClearExtraBrowserDocks(); + void LoadExtraBrowserDocks(); + void SaveExtraBrowserDocks(); + void ManageExtraBrowserDocks(); + void AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate); +#endif + + QIcon imageIcon; + QIcon colorIcon; + QIcon slideshowIcon; + QIcon audioInputIcon; + QIcon audioOutputIcon; + QIcon desktopCapIcon; + QIcon windowCapIcon; + QIcon gameCapIcon; + QIcon cameraIcon; + QIcon textIcon; + QIcon mediaIcon; + QIcon browserIcon; + QIcon groupIcon; + QIcon sceneIcon; + QIcon defaultIcon; + QIcon audioProcessOutputIcon; + + QIcon GetImageIcon() const; + QIcon GetColorIcon() const; + QIcon GetSlideshowIcon() const; + QIcon GetAudioInputIcon() const; + QIcon GetAudioOutputIcon() const; + QIcon GetDesktopCapIcon() const; + QIcon GetWindowCapIcon() const; + QIcon GetGameCapIcon() const; + QIcon GetCameraIcon() const; + QIcon GetTextIcon() const; + QIcon GetMediaIcon() const; + QIcon GetBrowserIcon() const; + QIcon GetDefaultIcon() const; + QIcon GetAudioProcessOutputIcon() const; + + QSlider *tBar; + bool tBarActive = false; + + OBSSource GetOverrideTransition(OBSSource source); + int GetOverrideTransitionDuration(OBSSource source); + + void UpdateProjectorHideCursor(); + void UpdateProjectorAlwaysOnTop(bool top); + void ResetProjectors(); + + QPointer screenshotData; + + void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); + + bool autoStartBroadcast = true; + bool autoStopBroadcast = true; + bool broadcastActive = false; + bool broadcastReady = false; + QPointer youtubeStreamCheckThread; +#ifdef YOUTUBE_ENABLED + void YoutubeStreamCheck(const std::string &key); + void ShowYouTubeAutoStartWarning(); + void YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now); +#endif + void BroadcastButtonClicked(); + void SetBroadcastFlowEnabled(bool enabled); + + void UpdatePreviewSafeAreas(); + bool drawSafeAreas = false; + + void CenterSelectedSceneItems(const CenterType ¢erType); + void ShowMissingFilesDialog(obs_missing_files_t *files); + + QColor selectionColor; + QColor cropColor; + QColor hoverColor; + + QColor GetCropColor() const; + QColor GetHoverColor() const; + + void UpdatePreviewSpacingHelpers(); + bool drawSpacingHelpers = true; + + float GetDevicePixelRatio(); + void SourceToolBarActionsSetEnabled(); + + std::string lastScreenshot; + std::string lastReplay; + + void UpdatePreviewOverflowSettings(); + void UpdatePreviewScrollbars(); + + bool streamingStarting = false; + + bool recordingStarted = false; + bool isRecordingPausable = false; + bool recordingPaused = false; + + bool restartingVCam = false; + +public slots: + void DeferSaveBegin(); + void DeferSaveEnd(); + + void DisplayStreamStartError(); + + void SetupBroadcast(); + + void StartStreaming(); + void StopStreaming(); + void ForceStopStreaming(); + + void StreamDelayStarting(int sec); + void StreamDelayStopping(int sec); + + void StreamingStart(); + void StreamStopping(); + void StreamingStop(int errorcode, QString last_error); + + void StartRecording(); + void StopRecording(); + + void RecordingStart(); + void RecordStopping(); + void RecordingStop(int code, QString last_error); + void RecordingFileChanged(QString lastRecordingPath); + + void ShowReplayBufferPauseWarning(); + void StartReplayBuffer(); + void StopReplayBuffer(); + + void ReplayBufferStart(); + void ReplayBufferSave(); + void ReplayBufferSaved(); + void ReplayBufferStopping(); + void ReplayBufferStop(int code); + + void StartVirtualCam(); + void StopVirtualCam(); + + void OnVirtualCamStart(); + void OnVirtualCamStop(int code); + + void SaveProjectDeferred(); + void SaveProject(); + + void SetTransition(OBSSource transition); + void OverrideTransition(OBSSource transition); + void TransitionToScene(OBSScene scene, bool force = false); + void TransitionToScene(OBSSource scene, bool force = false, bool quickTransition = false, int quickDuration = 0, + bool black = false, bool manual = false); + void SetCurrentScene(OBSSource scene, bool force = false); + + void UpdatePatronJson(const QString &text, const QString &error); + + void ShowContextBar(); + void HideContextBar(); + void PauseRecording(); + void UnpauseRecording(); + + void UpdateEditMenu(); + +private slots: + + void on_actionMainUndo_triggered(); + void on_actionMainRedo_triggered(); + + void AddSceneItem(OBSSceneItem item); + void AddScene(OBSSource source); + void RemoveScene(OBSSource source); + void RenameSources(OBSSource source, QString newName, QString prevName); + + void ActivateAudioSource(OBSSource source); + void DeactivateAudioSource(OBSSource source); + + void DuplicateSelectedScene(); + void RemoveSelectedScene(); + + void ToggleAlwaysOnTop(); + + void ReorderSources(OBSScene scene); + void RefreshSources(OBSScene scene); + + void ProcessHotkey(obs_hotkey_id id, bool pressed); + + void AddTransition(const char *id); + void RenameTransition(OBSSource transition); + void TransitionClicked(); + void TransitionStopped(); + void TransitionFullyStopped(); + void TriggerQuickTransition(int id); + + void SetDeinterlacingMode(); + void SetDeinterlacingOrder(); + + void SetScaleFilter(); + + void SetBlendingMethod(); + void SetBlendingMode(); + + void IconActivated(QSystemTrayIcon::ActivationReason reason); + void SetShowing(bool showing); + + void ToggleShowHide(); + + void HideAudioControl(); + void UnhideAllAudioControls(); + void ToggleHideMixer(); + + void MixerRenameSource(); + + void on_vMixerScrollArea_customContextMenuRequested(); + void on_hMixerScrollArea_customContextMenuRequested(); + + void on_actionCopySource_triggered(); + void on_actionPasteRef_triggered(); + void on_actionPasteDup_triggered(); + + void on_actionCopyFilters_triggered(); + void on_actionPasteFilters_triggered(); + void AudioMixerCopyFilters(); + void AudioMixerPasteFilters(); + void SourcePasteFilters(OBSSource source, OBSSource dstSource); + + void on_previewXScrollBar_valueChanged(int value); + void on_previewYScrollBar_valueChanged(int value); + + void PreviewScalingModeChanged(int value); + + void ColorChange(); + + SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); + + void on_actionShowAbout_triggered(); + + void EnablePreview(); + void DisablePreview(); + + void EnablePreviewProgram(); + void DisablePreviewProgram(); + + void SceneCopyFilters(); + void ScenePasteFilters(); + + void CheckDiskSpaceRemaining(); + void OpenSavedProjector(SavedProjectorInfo *info); + + void ResetStatsHotkey(); + + void SetImageIcon(const QIcon &icon); + void SetColorIcon(const QIcon &icon); + void SetSlideshowIcon(const QIcon &icon); + void SetAudioInputIcon(const QIcon &icon); + void SetAudioOutputIcon(const QIcon &icon); + void SetDesktopCapIcon(const QIcon &icon); + void SetWindowCapIcon(const QIcon &icon); + void SetGameCapIcon(const QIcon &icon); + void SetCameraIcon(const QIcon &icon); + void SetTextIcon(const QIcon &icon); + void SetMediaIcon(const QIcon &icon); + void SetBrowserIcon(const QIcon &icon); + void SetGroupIcon(const QIcon &icon); + void SetSceneIcon(const QIcon &icon); + void SetDefaultIcon(const QIcon &icon); + void SetAudioProcessOutputIcon(const QIcon &icon); + + void TBarChanged(int value); + void TBarReleased(); + + void LockVolumeControl(bool lock); + + void UpdateVirtualCamConfig(const VCamConfig &config); + void RestartVirtualCam(const VCamConfig &config); + void RestartingVirtualCam(); + +private: + /* OBS Callbacks */ + static void SceneReordered(void *data, calldata_t *params); + static void SceneRefreshed(void *data, calldata_t *params); + static void SceneItemAdded(void *data, calldata_t *params); + static void SourceCreated(void *data, calldata_t *params); + static void SourceRemoved(void *data, calldata_t *params); + static void SourceActivated(void *data, calldata_t *params); + static void SourceDeactivated(void *data, calldata_t *params); + static void SourceAudioActivated(void *data, calldata_t *params); + static void SourceAudioDeactivated(void *data, calldata_t *params); + static void SourceRenamed(void *data, calldata_t *params); + static void RenderMain(void *data, uint32_t cx, uint32_t cy); + + void ResizePreview(uint32_t cx, uint32_t cy); + + void AddSource(const char *id); + QMenu *CreateAddSourcePopupMenu(); + void AddSourcePopupMenu(const QPoint &pos); + void copyActionsDynamicProperties(); + + static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); + + void AutoRemux(QString input, bool no_show = false); + + void UpdateIsRecordingPausable(); + + bool IsFFmpegOutputToURL() const; + bool OutputPathValid(); + void OutputPathInvalidMessage(); + + bool LowDiskSpace(); + void DiskSpaceMessage(); + + OBSSource prevFTBSource = nullptr; + + float dpi = 1.0; + +public: + OBSSource GetProgramSource(); + OBSScene GetCurrentScene(); + + void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); + + inline OBSSource GetCurrentSceneSource() + { + OBSScene curScene = GetCurrentScene(); + return OBSSource(obs_scene_get_source(curScene)); + } + + obs_service_t *GetService(); + void SetService(obs_service_t *service); + + int GetTransitionDuration(); + int GetTbarPosition(); + + inline bool IsPreviewProgramMode() const { return os_atomic_load_bool(&previewProgramMode); } + + inline bool VCamEnabled() const { return vcamEnabled; } + + bool Active() const; + + void ResetUI(); + int ResetVideo(); + bool ResetAudio(); + + void ResetOutputs(); + + void RefreshVolumeColors(); + + void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); + + void NewProject(); + void LoadProject(); + + inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) + { + x = previewX; + y = previewY; + cx = previewCX; + cy = previewCY; + } + + inline bool SavingDisabled() const { return disableSaving; } + + inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); } + + void SaveService(); + bool LoadService(); + + inline Auth *GetAuth() { return auth.get(); } + + inline void EnableOutputs(bool enable) + { + if (enable) { + if (--disableOutputsRef < 0) + disableOutputsRef = 0; + } else { + disableOutputsRef++; + } + } + + QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); + QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item); + void CreateSourcePopupMenu(int idx, bool preview); + + void UpdateTitleBar(); + + void SystemTrayInit(); + void SystemTray(bool firstStarted); + + void OpenSavedProjectors(); + + void CreateInteractionWindow(obs_source_t *source); + void CreatePropertiesWindow(obs_source_t *source); + void CreateFiltersWindow(obs_source_t *source); + void CreateEditTransformWindow(obs_sceneitem_t *item); + + QAction *AddDockWidget(QDockWidget *dock); + void AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser = false); + void RemoveDockWidget(const QString &name); + bool IsDockObjectNameUsed(const QString &name); + void AddCustomDockWidget(QDockWidget *dock); + + static OBSBasic *Get(); + + const char *GetCurrentOutputPath(); + + void DeleteProjector(OBSProjector *projector); + + static QList GetProjectorMenuMonitorsFormatted(); + template + static void AddProjectorMenuMonitors(QMenu *parent, Receiver *target, void (Receiver::*slot)(Args...)) + { + auto projectors = GetProjectorMenuMonitorsFormatted(); + for (int i = 0; i < projectors.size(); i++) { + QString str = projectors[i]; + QAction *action = parent->addAction(str, target, slot); + action->setProperty("monitor", i); + } + } + + QIcon GetSourceIcon(const char *id) const; + QIcon GetGroupIcon() const; + QIcon GetSceneIcon() const; + + OBSWeakSource copyFilter; + + void ShowStatusBarMessage(const QString &message); + + static OBSData BackupScene(obs_scene_t *scene, std::vector *sources = nullptr); + void CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data); + + static inline OBSData BackupScene(obs_source_t *scene_source, std::vector *sources = nullptr) + { + obs_scene_t *scene = obs_scene_from_source(scene_source); + return BackupScene(scene, sources); + } + + void CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array); + + void SetDisplayAffinity(QWindow *window); + + QColor GetSelectionColor() const; + inline bool Closing() { return closing; } + +protected: + virtual void closeEvent(QCloseEvent *event) override; + virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; + virtual void changeEvent(QEvent *event) override; + +private slots: + void on_actionFullscreenInterface_triggered(); + + void on_actionShow_Recordings_triggered(); + void on_actionRemux_triggered(); + void on_action_Settings_triggered(); + void on_actionShowMacPermissions_triggered(); + void on_actionShowMissingFiles_triggered(); + void on_actionAdvAudioProperties_triggered(); + void on_actionMixerToolbarAdvAudio_triggered(); + void on_actionMixerToolbarMenu_triggered(); + void on_actionShowLogs_triggered(); + void on_actionUploadCurrentLog_triggered(); + void on_actionUploadLastLog_triggered(); + void on_actionViewCurrentLog_triggered(); + void on_actionCheckForUpdates_triggered(); + void on_actionRepair_triggered(); + void on_actionShowWhatsNew_triggered(); + void on_actionRestartSafe_triggered(); + + void on_actionShowCrashLogs_triggered(); + void on_actionUploadLastCrashLog_triggered(); + + void on_actionEditTransform_triggered(); + void on_actionCopyTransform_triggered(); + void on_actionPasteTransform_triggered(); + void on_actionRotate90CW_triggered(); + void on_actionRotate90CCW_triggered(); + void on_actionRotate180_triggered(); + void on_actionFlipHorizontal_triggered(); + void on_actionFlipVertical_triggered(); + void on_actionFitToScreen_triggered(); + void on_actionStretchToScreen_triggered(); + void on_actionCenterToScreen_triggered(); + void on_actionVerticalCenter_triggered(); + void on_actionHorizontalCenter_triggered(); + void on_actionSceneFilters_triggered(); + + void on_OBSBasic_customContextMenuRequested(const QPoint &pos); + + void on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *prev); + void on_scenes_customContextMenuRequested(const QPoint &pos); + void GridActionClicked(); + void on_actionSceneListMode_triggered(); + void on_actionSceneGridMode_triggered(); + void on_actionAddScene_triggered(); + void on_actionRemoveScene_triggered(); + void on_actionSceneUp_triggered(); + void on_actionSceneDown_triggered(); + void on_sources_customContextMenuRequested(const QPoint &pos); + void on_scenes_itemDoubleClicked(QListWidgetItem *item); + void on_actionAddSource_triggered(); + void on_actionRemoveSource_triggered(); + void on_actionInteract_triggered(); + void on_actionSourceProperties_triggered(); + void on_actionSourceUp_triggered(); + void on_actionSourceDown_triggered(); + + void on_actionMoveUp_triggered(); + void on_actionMoveDown_triggered(); + void on_actionMoveToTop_triggered(); + void on_actionMoveToBottom_triggered(); + + void on_actionLockPreview_triggered(); + + void on_scalingMenu_aboutToShow(); + void on_actionScaleWindow_triggered(); + void on_actionScaleCanvas_triggered(); + void on_actionScaleOutput_triggered(); + + void Screenshot(OBSSource source_ = nullptr); + void ScreenshotSelectedSource(); + void ScreenshotProgram(); + void ScreenshotScene(); + + void on_actionHelpPortal_triggered(); + void on_actionWebsite_triggered(); + void on_actionDiscord_triggered(); + void on_actionReleaseNotes_triggered(); + + void on_preview_customContextMenuRequested(); + void ProgramViewContextMenuRequested(); + void on_previewDisabledWidget_customContextMenuRequested(); + + void on_actionShowSettingsFolder_triggered(); + void on_actionShowProfileFolder_triggered(); + + void on_actionAlwaysOnTop_triggered(); + + void on_toggleListboxToolbars_toggled(bool visible); + void on_toggleContextBar_toggled(bool visible); + void on_toggleStatusBar_toggled(bool visible); + void on_toggleSourceIcons_toggled(bool visible); + + void on_transitions_currentIndexChanged(int index); + void on_transitionAdd_clicked(); + void on_transitionRemove_clicked(); + void on_transitionProps_clicked(); + void on_transitionDuration_valueChanged(); + + void ShowTransitionProperties(); + void HideTransitionProperties(); + + // Source Context Buttons + void on_sourcePropertiesButton_clicked(); + void on_sourceFiltersButton_clicked(); + void on_sourceInteractButton_clicked(); + + void on_autoConfigure_triggered(); + void on_stats_triggered(); + + void on_resetUI_triggered(); + void on_resetDocks_triggered(bool force = false); + void on_lockDocks_toggled(bool lock); + void on_multiviewProjectorWindowed_triggered(); + void on_sideDocks_toggled(bool side); + + void logUploadFinished(const QString &text, const QString &error); + void crashUploadFinished(const QString &text, const QString &error); + void openLogDialog(const QString &text, const bool crash); + + void updateCheckFinished(); + + void MoveSceneToTop(); + void MoveSceneToBottom(); + + void EditSceneName(); + void EditSceneItemName(); + + void SceneNameEdited(QWidget *editor); + + void OpenSceneFilters(); + void OpenFilters(OBSSource source = nullptr); + void OpenProperties(OBSSource source = nullptr); + void OpenInteraction(OBSSource source = nullptr); + void OpenEditTransform(OBSSceneItem item = nullptr); + + void EnablePreviewDisplay(bool enable); + void TogglePreview(); + + void OpenStudioProgramProjector(); + void OpenPreviewProjector(); + void OpenSourceProjector(); + void OpenMultiviewProjector(); + void OpenSceneProjector(); + + void OpenStudioProgramWindow(); + void OpenPreviewWindow(); + void OpenSourceWindow(); + void OpenSceneWindow(); + + void StackedMixerAreaContextMenuRequested(); + + void ResizeOutputSizeOfSource(); + + void RepairOldExtraDockName(); + void RepairCustomExtraDockName(); + + /* Stream action (start/stop) slot */ + void StreamActionTriggered(); + + /* Record action (start/stop) slot */ + void RecordActionTriggered(); + + /* Record pause (pause/unpause) slot */ + void RecordPauseToggled(); + + /* Replay Buffer action (start/stop) slot */ + void ReplayBufferActionTriggered(); + + /* Virtual Cam action (start/stop) slots */ + void VirtualCamActionTriggered(); + + void OpenVirtualCamConfig(); + + /* Studio Mode toggle slot */ + void TogglePreviewProgramMode(); + +public slots: + void on_actionResetTransform_triggered(); + + bool StreamingActive(); + bool RecordingActive(); + bool ReplayBufferActive(); + bool VirtualCamActive(); + + void ClearContextBar(); + void UpdateContextBar(bool force = false); + void UpdateContextBarDeferred(bool force = false); + void UpdateContextBarVisibility(); + +signals: + /* Streaming signals */ + void StreamingPreparing(); + void StreamingStarting(bool broadcastAutoStart); + void StreamingStarted(bool withDelay = false); + void StreamingStopping(); + void StreamingStopped(bool withDelay = false); + + /* Broadcast Flow signals */ + void BroadcastFlowEnabled(bool enabled); + void BroadcastStreamReady(bool ready); + void BroadcastStreamActive(); + void BroadcastStreamStarted(bool autoStop); + + /* Recording signals */ + void RecordingStarted(bool pausable = false); + void RecordingPaused(); + void RecordingUnpaused(); + void RecordingStopping(); + void RecordingStopped(); + + /* Replay Buffer signals */ + void ReplayBufEnabled(bool enabled); + void ReplayBufStarted(); + void ReplayBufStopping(); + void ReplayBufStopped(); + + /* Virtual Camera signals */ + void VirtualCamEnabled(); + void VirtualCamStarted(); + void VirtualCamStopped(); + + /* Studio Mode signal */ + void PreviewProgramModeChanged(bool enabled); + void CanvasResized(uint32_t width, uint32_t height); + void OutputResized(uint32_t width, uint32_t height); + + /* Preview signals */ + void PreviewXScrollBarMoved(int value); + void PreviewYScrollBarMoved(int value); + +private: + std::unique_ptr ui; + + QPointer controlsDock; + +public: + /* `undo_s` needs to be declared after `ui` to prevent an uninitialized + * warning for `ui` while initializing `undo_s`. */ + undo_stack undo_s; + + explicit OBSBasic(QWidget *parent = 0); + virtual ~OBSBasic(); + + virtual void OBSInit() override; + + virtual config_t *Config() const override; + + virtual int GetProfilePath(char *path, size_t size, const char *file) const override; + + static void InitBrowserPanelSafeBlock(); +#ifdef YOUTUBE_ENABLED + void NewYouTubeAppDock(); + void DeleteYouTubeAppDock(); + YouTubeAppDock *GetYouTubeAppDock(); +#endif + // MARK: - Generic UI Helper Functions + OBSPromptResult PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback); + + // MARK: - OBS Profile Management +private: + OBSProfileCache profiles{}; + + void SetupNewProfile(const std::string &profileName, bool useWizard = false); + void SetupDuplicateProfile(const std::string &profileName); + void SetupRenameProfile(const std::string &profileName); + + const OBSProfile &CreateProfile(const std::string &profileName); + void RemoveProfile(OBSProfile profile); + + void ChangeProfile(); + + void RefreshProfileCache(); + + void RefreshProfiles(bool refreshCache = false); + + void ActivateProfile(const OBSProfile &profile, bool reset = false); + void UpdateProfileEncoders(); + std::vector GetRestartRequirements(const ConfigFile &config) const; + void ResetProfileData(); + void CheckForSimpleModeX264Fallback(); + +public: + inline const OBSProfileCache &GetProfileCache() const noexcept { return profiles; }; + + const OBSProfile &GetCurrentProfile() const; + + std::optional GetProfileByName(const std::string &profileName) const; + std::optional GetProfileByDirectoryName(const std::string &directoryName) const; + +private slots: + void on_actionNewProfile_triggered(); + void on_actionDupProfile_triggered(); + void on_actionRenameProfile_triggered(); + void on_actionRemoveProfile_triggered(bool skipConfirmation = false); + void on_actionImportProfile_triggered(); + void on_actionExportProfile_triggered(); + +public slots: + bool CreateNewProfile(const QString &name); + bool CreateDuplicateProfile(const QString &name); + void DeleteProfile(const QString &profileName); + + // MARK: - OBS Scene Collection Management +private: + OBSSceneCollectionCache collections{}; + + void SetupNewSceneCollection(const std::string &collectionName); + void SetupDuplicateSceneCollection(const std::string &collectionName); + void SetupRenameSceneCollection(const std::string &collectionName); + + const OBSSceneCollection &CreateSceneCollection(const std::string &collectionName); + void RemoveSceneCollection(OBSSceneCollection collection); + + bool CreateDuplicateSceneCollection(const QString &name); + void DeleteSceneCollection(const QString &name); + void ChangeSceneCollection(); + + void RefreshSceneCollectionCache(); + + void RefreshSceneCollections(bool refreshCache = false); + void ActivateSceneCollection(const OBSSceneCollection &collection); + +public: + inline const OBSSceneCollectionCache &GetSceneCollectionCache() const noexcept { return collections; }; + + const OBSSceneCollection &GetCurrentSceneCollection() const; + + std::optional GetSceneCollectionByName(const std::string &collectionName) const; + std::optional GetSceneCollectionByFileName(const std::string &fileName) const; + +private slots: + void on_actionNewSceneCollection_triggered(); + void on_actionDupSceneCollection_triggered(); + void on_actionRenameSceneCollection_triggered(); + void on_actionRemoveSceneCollection_triggered(bool skipConfirmation = false); + void on_actionImportSceneCollection_triggered(); + void on_actionExportSceneCollection_triggered(); + void on_actionRemigrateSceneCollection_triggered(); + +public slots: + bool CreateNewSceneCollection(const QString &name); +}; + +extern bool cef_js_avail; + +class SceneRenameDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + SceneRenameDelegate(QObject *parent); + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + +protected: + virtual bool eventFilter(QObject *editor, QEvent *event) override; +}; diff --git a/frontend/widgets/OBSBasic.cpp b/frontend/widgets/OBSBasic.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp new file mode 100644 index 000000000..81bb7b478 --- /dev/null +++ b/frontend/widgets/OBSBasic.hpp @@ -0,0 +1,1382 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "window-main.hpp" +#include "window-basic-interaction.hpp" +#include "window-basic-vcam.hpp" +#include "window-basic-properties.hpp" +#include "window-basic-transform.hpp" +#include "window-basic-adv-audio.hpp" +#include "window-basic-filters.hpp" +#include "window-missing-files.hpp" +#include "window-projector.hpp" +#include "window-basic-about.hpp" +#ifdef YOUTUBE_ENABLED +#include "window-dock-youtube-app.hpp" +#endif +#include "auth-base.hpp" +#include "log-viewer.hpp" +#include "undo-stack-obs.hpp" + +#include + +#include +#include +#include + +#include + +class QMessageBox; +class QListWidgetItem; +class VolControl; +class OBSBasicStats; +class OBSBasicVCamConfig; + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#define DESKTOP_AUDIO_1 Str("DesktopAudioDevice1") +#define DESKTOP_AUDIO_2 Str("DesktopAudioDevice2") +#define AUX_AUDIO_1 Str("AuxAudioDevice1") +#define AUX_AUDIO_2 Str("AuxAudioDevice2") +#define AUX_AUDIO_3 Str("AuxAudioDevice3") +#define AUX_AUDIO_4 Str("AuxAudioDevice4") + +#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" + +#define PREVIEW_EDGE_SIZE 10 + +struct BasicOutputHandler; + +enum class QtDataRole { + OBSRef = Qt::UserRole, + OBSSignals, +}; + +struct SavedProjectorInfo { + ProjectorType type; + int monitor; + std::string geometry; + std::string name; + bool alwaysOnTop; + bool alwaysOnTopOverridden; +}; + +struct SourceCopyInfo { + OBSWeakSource weak_source; + bool visible; + obs_sceneitem_crop crop; + obs_transform_info transform; + obs_blending_method blend_method; + obs_blending_type blend_mode; +}; + +struct QuickTransition { + QPushButton *button = nullptr; + OBSSource source; + obs_hotkey_id hotkey = OBS_INVALID_HOTKEY_ID; + int duration = 0; + int id = 0; + bool fadeToBlack = false; + + inline QuickTransition() {} + inline QuickTransition(OBSSource source_, int duration_, int id_, bool fadeToBlack_ = false) + : source(source_), + duration(duration_), + id(id_), + fadeToBlack(fadeToBlack_), + renamedSignal(std::make_shared(obs_source_get_signal_handler(source), "rename", + SourceRenamed, this)) + { + } + +private: + static void SourceRenamed(void *param, calldata_t *data); + std::shared_ptr renamedSignal; +}; + +struct OBSProfile { + std::string name; + std::string directoryName; + std::filesystem::path path; + std::filesystem::path profileFile; +}; + +struct OBSSceneCollection { + std::string name; + std::string fileName; + std::filesystem::path collectionFile; +}; + +struct OBSPromptResult { + bool success; + std::string promptValue; + bool optionValue; +}; + +struct OBSPromptRequest { + std::string title; + std::string prompt; + std::string promptValue; + bool withOption; + std::string optionPrompt; + bool optionValue; +}; + +using OBSPromptCallback = std::function; + +using OBSProfileCache = std::map; +using OBSSceneCollectionCache = std::map; + +class ColorSelect : public QWidget { + +public: + explicit ColorSelect(QWidget *parent = 0); + +private: + std::unique_ptr ui; +}; + +class OBSBasic : public OBSMainWindow { + Q_OBJECT + Q_PROPERTY(QIcon imageIcon READ GetImageIcon WRITE SetImageIcon DESIGNABLE true) + Q_PROPERTY(QIcon colorIcon READ GetColorIcon WRITE SetColorIcon DESIGNABLE true) + Q_PROPERTY(QIcon slideshowIcon READ GetSlideshowIcon WRITE SetSlideshowIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioInputIcon READ GetAudioInputIcon WRITE SetAudioInputIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioOutputIcon READ GetAudioOutputIcon WRITE SetAudioOutputIcon DESIGNABLE true) + Q_PROPERTY(QIcon desktopCapIcon READ GetDesktopCapIcon WRITE SetDesktopCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon windowCapIcon READ GetWindowCapIcon WRITE SetWindowCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon gameCapIcon READ GetGameCapIcon WRITE SetGameCapIcon DESIGNABLE true) + Q_PROPERTY(QIcon cameraIcon READ GetCameraIcon WRITE SetCameraIcon DESIGNABLE true) + Q_PROPERTY(QIcon textIcon READ GetTextIcon WRITE SetTextIcon DESIGNABLE true) + Q_PROPERTY(QIcon mediaIcon READ GetMediaIcon WRITE SetMediaIcon DESIGNABLE true) + Q_PROPERTY(QIcon browserIcon READ GetBrowserIcon WRITE SetBrowserIcon DESIGNABLE true) + Q_PROPERTY(QIcon groupIcon READ GetGroupIcon WRITE SetGroupIcon DESIGNABLE true) + Q_PROPERTY(QIcon sceneIcon READ GetSceneIcon WRITE SetSceneIcon DESIGNABLE true) + Q_PROPERTY(QIcon defaultIcon READ GetDefaultIcon WRITE SetDefaultIcon DESIGNABLE true) + Q_PROPERTY(QIcon audioProcessOutputIcon READ GetAudioProcessOutputIcon WRITE SetAudioProcessOutputIcon + DESIGNABLE true) + + friend class OBSAbout; + friend class OBSBasicPreview; + friend class OBSBasicStatusBar; + friend class OBSBasicSourceSelect; + friend class OBSBasicTransform; + friend class OBSBasicSettings; + friend class Auth; + friend class AutoConfig; + friend class AutoConfigStreamPage; + friend class RecordButton; + friend class ControlsSplitButton; + friend class ExtraBrowsersModel; + friend class ExtraBrowsersDelegate; + friend class DeviceCaptureToolbar; + friend class OBSBasicSourceSelect; + friend class OBSYoutubeActions; + friend class OBSPermissions; + friend struct BasicOutputHandler; + friend struct OBSStudioAPI; + friend class ScreenshotObj; + + enum class MoveDir { Up, Down, Left, Right }; + + enum DropType { + DropType_RawText, + DropType_Text, + DropType_Image, + DropType_Media, + DropType_Html, + DropType_Url, + }; + + enum ContextBarSize { ContextBarSize_Minimized, ContextBarSize_Reduced, ContextBarSize_Normal }; + + enum class CenterType { + Scene, + Vertical, + Horizontal, + }; + +private: + obs_frontend_callbacks *api = nullptr; + + std::shared_ptr auth; + + std::vector volumes; + + std::vector signalHandlers; + + QList> oldExtraDocks; + QStringList oldExtraDockNames; + + OBSDataAutoRelease collectionModuleData; + std::vector safeModeTransitions; + + bool loaded = false; + long disableSaving = 1; + bool projectChanged = false; + bool previewEnabled = true; + ContextBarSize contextBarSize = ContextBarSize_Normal; + + std::deque clipboard; + OBSWeakSourceAutoRelease copyFiltersSource; + bool copyVisible = true; + obs_transform_info copiedTransformInfo; + obs_sceneitem_crop copiedCropInfo; + bool hasCopiedTransform = false; + OBSWeakSourceAutoRelease copySourceTransition; + int copySourceTransitionDuration; + + bool closing = false; + bool clearingFailed = false; + + QScopedPointer devicePropertiesThread; + QScopedPointer whatsNewInitThread; + QScopedPointer updateCheckThread; + QScopedPointer introCheckThread; + QScopedPointer logUploadThread; + + QPointer interaction; + QPointer properties; + QPointer transformWindow; + QPointer advAudioWindow; + QPointer filters; + QPointer statsDock; +#ifdef YOUTUBE_ENABLED + QPointer youtubeAppDock; + uint64_t lastYouTubeAppDockCreationTime = 0; +#endif + QPointer about; + QPointer missDialog; + QPointer logView; + + QPointer cpuUsageTimer; + QPointer diskFullTimer; + + QPointer nudge_timer; + bool recent_nudge = false; + + os_cpu_usage_info_t *cpuUsageInfo = nullptr; + + OBSService service; + std::unique_ptr outputHandler; + std::shared_future setupStreamingGuard; + bool streamingStopping = false; + bool recordingStopping = false; + bool replayBufferStopping = false; + + gs_vertbuffer_t *box = nullptr; + gs_vertbuffer_t *boxLeft = nullptr; + gs_vertbuffer_t *boxTop = nullptr; + gs_vertbuffer_t *boxRight = nullptr; + gs_vertbuffer_t *boxBottom = nullptr; + gs_vertbuffer_t *circle = nullptr; + + gs_vertbuffer_t *actionSafeMargin = nullptr; + gs_vertbuffer_t *graphicsSafeMargin = nullptr; + gs_vertbuffer_t *fourByThreeSafeMargin = nullptr; + gs_vertbuffer_t *leftLine = nullptr; + gs_vertbuffer_t *topLine = nullptr; + gs_vertbuffer_t *rightLine = nullptr; + + int previewX = 0, previewY = 0; + int previewCX = 0, previewCY = 0; + float previewScale = 0.0f; + + ConfigFile activeConfiguration; + + std::vector savedProjectorsArray; + std::vector projectors; + + QPointer stats; + QPointer remux; + QPointer extraBrowsers; + QPointer importer; + + QPointer transitionButton; + + bool vcamEnabled = false; + VCamConfig vcamConfig; + + QScopedPointer trayIcon; + QPointer sysTrayStream; + QPointer sysTrayRecord; + QPointer sysTrayReplayBuffer; + QPointer sysTrayVirtualCam; + QPointer showHide; + QPointer exit; + QPointer trayMenu; + QPointer previewProjector; + QPointer studioProgramProjector; + QPointer previewProjectorSource; + QPointer previewProjectorMain; + QPointer sceneProjectorMenu; + QPointer sourceProjector; + QPointer scaleFilteringMenu; + QPointer blendingMethodMenu; + QPointer blendingModeMenu; + QPointer colorMenu; + QPointer colorWidgetAction; + QPointer colorSelect; + QPointer deinterlaceMenu; + QPointer perSceneTransitionMenu; + QPointer shortcutFilter; + QPointer renameScene; + QPointer renameSource; + + QPointer programWidget; + QPointer programLayout; + QPointer programLabel; + + QScopedPointer patronJsonThread; + std::string patronJson; + + std::atomic currentScene = nullptr; + std::optional> lastOutputResolution; + std::optional> migrationBaseResolution; + bool usingAbsoluteCoordinates = false; + + void DisableRelativeCoordinates(bool disable); + + void OnEvent(enum obs_frontend_event event); + + void UpdateMultiviewProjectorMenu(); + + void DrawBackdrop(float cx, float cy); + + void SetupEncoders(); + + void CreateFirstRunSources(); + void CreateDefaultScene(bool firstStart); + + void UpdateVolumeControlsDecayRate(); + void UpdateVolumeControlsPeakMeterType(); + void ClearVolumeControls(); + + void UploadLog(const char *subdir, const char *file, const bool crash); + + void Save(const char *file); + void LoadData(obs_data_t *data, const char *file, bool remigrate = false); + void Load(const char *file, bool remigrate = false); + + void InitHotkeys(); + void CreateHotkeys(); + void ClearHotkeys(); + + bool InitService(); + + bool InitBasicConfigDefaults(); + void InitBasicConfigDefaults2(); + bool InitBasicConfig(); + + void InitOBSCallbacks(); + + void InitPrimitives(); + + void OnFirstLoad(); + + OBSSceneItem GetSceneItem(QListWidgetItem *item); + OBSSceneItem GetCurrentSceneItem(); + + bool QueryRemoveSource(obs_source_t *source); + + void TimedCheckForUpdates(); + void CheckForUpdates(bool manualUpdate); + + void GetFPSCommon(uint32_t &num, uint32_t &den) const; + void GetFPSInteger(uint32_t &num, uint32_t &den) const; + void GetFPSFraction(uint32_t &num, uint32_t &den) const; + void GetFPSNanoseconds(uint32_t &num, uint32_t &den) const; + void GetConfigFPS(uint32_t &num, uint32_t &den) const; + + void UpdatePreviewScalingMenu(); + + void LoadSceneListOrder(obs_data_array_t *array); + obs_data_array_t *SaveSceneListOrder(); + void ChangeSceneIndex(bool relative, int idx, int invalidIdx); + + void TempFileOutput(const char *path, int vBitrate, int aBitrate); + void TempStreamOutput(const char *url, const char *key, int vBitrate, int aBitrate); + + void CloseDialogs(); + void ClearSceneData(); + void ClearProjectors(); + + void Nudge(int dist, MoveDir dir); + + OBSProjector *OpenProjector(obs_source_t *source, int monitor, ProjectorType type); + + void GetAudioSourceFilters(); + void GetAudioSourceProperties(); + void VolControlContextMenu(); + void ToggleVolControlLayout(); + void ToggleMixerLayout(bool vertical); + + void LogScenes(); + void SaveProjectNow(); + + int GetTopSelectedSourceItem(); + + QModelIndexList GetAllSelectedSourceItems(); + + obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, replayBufHotkeys, vcamHotkeys, + togglePreviewHotkeys, contextBarHotkeys; + obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey, addChapterHotkey; + + void InitDefaultTransitions(); + void InitTransition(obs_source_t *transition); + obs_source_t *FindTransition(const char *name); + OBSSource GetCurrentTransition(); + obs_data_array_t *SaveTransitions(); + void LoadTransitions(obs_data_array_t *transitions, obs_load_source_cb cb, void *private_data); + + obs_source_t *fadeTransition; + obs_source_t *cutTransition; + + void CreateProgramDisplay(); + void CreateProgramOptions(); + void AddQuickTransitionId(int id); + void AddQuickTransition(); + void AddQuickTransitionHotkey(QuickTransition *qt); + void RemoveQuickTransitionHotkey(QuickTransition *qt); + void LoadQuickTransitions(obs_data_array_t *array); + obs_data_array_t *SaveQuickTransitions(); + void ClearQuickTransitionWidgets(); + void RefreshQuickTransitions(); + void DisableQuickTransitionWidgets(); + void EnableTransitionWidgets(bool enable); + void CreateDefaultQuickTransitions(); + + void PasteShowHideTransition(obs_sceneitem_t *item, bool show, obs_source_t *tr, int duration); + QMenu *CreatePerSceneTransitionMenu(); + QMenu *CreateVisibilityTransitionMenu(bool visible); + + QuickTransition *GetQuickTransition(int id); + int GetQuickTransitionIdx(int id); + QMenu *CreateTransitionMenu(QWidget *parent, QuickTransition *qt); + void ClearQuickTransitions(); + void QuickTransitionClicked(); + void QuickTransitionChange(); + void QuickTransitionChangeDuration(int value); + void QuickTransitionRemoveClicked(); + + void SetPreviewProgramMode(bool enabled); + void ResizeProgram(uint32_t cx, uint32_t cy); + void SetCurrentScene(obs_scene_t *scene, bool force = false); + static void RenderProgram(void *data, uint32_t cx, uint32_t cy); + + std::vector quickTransitions; + QPointer programOptions; + QPointer program; + OBSWeakSource lastScene; + OBSWeakSource swapScene; + OBSWeakSource programScene; + OBSWeakSource lastProgramScene; + bool editPropertiesMode = false; + bool sceneDuplicationMode = true; + bool swapScenesMode = true; + volatile bool previewProgramMode = false; + obs_hotkey_pair_id togglePreviewProgramHotkeys = 0; + obs_hotkey_id transitionHotkey = 0; + obs_hotkey_id statsHotkey = 0; + obs_hotkey_id screenshotHotkey = 0; + obs_hotkey_id sourceScreenshotHotkey = 0; + int quickTransitionIdCounter = 1; + bool overridingTransition = false; + + int programX = 0, programY = 0; + int programCX = 0, programCY = 0; + float programScale = 0.0f; + + int disableOutputsRef = 0; + + inline void OnActivate(bool force = false); + inline void OnDeactivate(); + + void AddDropSource(const char *file, DropType image); + void AddDropURL(const char *url, QString &name, obs_data_t *settings, const obs_video_info &ovi); + void ConfirmDropUrl(const QString &url); + void dragEnterEvent(QDragEnterEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; + + bool sysTrayMinimizeToTray(); + + void EnumDialogs(); + + QList visDialogs; + QList modalDialogs; + QList visMsgBoxes; + + QList visDlgPositions; + + QByteArray startingDockLayout; + + obs_data_array_t *SaveProjectors(); + void LoadSavedProjectors(obs_data_array_t *savedProjectors); + + void MacBranchesFetched(const QString &branch, bool manualUpdate); + void ReceivedIntroJson(const QString &text); + void ShowWhatsNew(const QString &url); + + void UpdatePreviewProgramIndicators(); + + QStringList extraDockNames; + QList> extraDocks; + + QStringList extraCustomDockNames; + QList> extraCustomDocks; + +#ifdef BROWSER_AVAILABLE + QPointer extraBrowserMenuDocksSeparator; + + QList> extraBrowserDocks; + QStringList extraBrowserDockNames; + QStringList extraBrowserDockTargets; + + void ClearExtraBrowserDocks(); + void LoadExtraBrowserDocks(); + void SaveExtraBrowserDocks(); + void ManageExtraBrowserDocks(); + void AddExtraBrowserDock(const QString &title, const QString &url, const QString &uuid, bool firstCreate); +#endif + + QIcon imageIcon; + QIcon colorIcon; + QIcon slideshowIcon; + QIcon audioInputIcon; + QIcon audioOutputIcon; + QIcon desktopCapIcon; + QIcon windowCapIcon; + QIcon gameCapIcon; + QIcon cameraIcon; + QIcon textIcon; + QIcon mediaIcon; + QIcon browserIcon; + QIcon groupIcon; + QIcon sceneIcon; + QIcon defaultIcon; + QIcon audioProcessOutputIcon; + + QIcon GetImageIcon() const; + QIcon GetColorIcon() const; + QIcon GetSlideshowIcon() const; + QIcon GetAudioInputIcon() const; + QIcon GetAudioOutputIcon() const; + QIcon GetDesktopCapIcon() const; + QIcon GetWindowCapIcon() const; + QIcon GetGameCapIcon() const; + QIcon GetCameraIcon() const; + QIcon GetTextIcon() const; + QIcon GetMediaIcon() const; + QIcon GetBrowserIcon() const; + QIcon GetDefaultIcon() const; + QIcon GetAudioProcessOutputIcon() const; + + QSlider *tBar; + bool tBarActive = false; + + OBSSource GetOverrideTransition(OBSSource source); + int GetOverrideTransitionDuration(OBSSource source); + + void UpdateProjectorHideCursor(); + void UpdateProjectorAlwaysOnTop(bool top); + void ResetProjectors(); + + QPointer screenshotData; + + void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); + + bool autoStartBroadcast = true; + bool autoStopBroadcast = true; + bool broadcastActive = false; + bool broadcastReady = false; + QPointer youtubeStreamCheckThread; +#ifdef YOUTUBE_ENABLED + void YoutubeStreamCheck(const std::string &key); + void ShowYouTubeAutoStartWarning(); + void YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now); +#endif + void BroadcastButtonClicked(); + void SetBroadcastFlowEnabled(bool enabled); + + void UpdatePreviewSafeAreas(); + bool drawSafeAreas = false; + + void CenterSelectedSceneItems(const CenterType ¢erType); + void ShowMissingFilesDialog(obs_missing_files_t *files); + + QColor selectionColor; + QColor cropColor; + QColor hoverColor; + + QColor GetCropColor() const; + QColor GetHoverColor() const; + + void UpdatePreviewSpacingHelpers(); + bool drawSpacingHelpers = true; + + float GetDevicePixelRatio(); + void SourceToolBarActionsSetEnabled(); + + std::string lastScreenshot; + std::string lastReplay; + + void UpdatePreviewOverflowSettings(); + void UpdatePreviewScrollbars(); + + bool streamingStarting = false; + + bool recordingStarted = false; + bool isRecordingPausable = false; + bool recordingPaused = false; + + bool restartingVCam = false; + +public slots: + void DeferSaveBegin(); + void DeferSaveEnd(); + + void DisplayStreamStartError(); + + void SetupBroadcast(); + + void StartStreaming(); + void StopStreaming(); + void ForceStopStreaming(); + + void StreamDelayStarting(int sec); + void StreamDelayStopping(int sec); + + void StreamingStart(); + void StreamStopping(); + void StreamingStop(int errorcode, QString last_error); + + void StartRecording(); + void StopRecording(); + + void RecordingStart(); + void RecordStopping(); + void RecordingStop(int code, QString last_error); + void RecordingFileChanged(QString lastRecordingPath); + + void ShowReplayBufferPauseWarning(); + void StartReplayBuffer(); + void StopReplayBuffer(); + + void ReplayBufferStart(); + void ReplayBufferSave(); + void ReplayBufferSaved(); + void ReplayBufferStopping(); + void ReplayBufferStop(int code); + + void StartVirtualCam(); + void StopVirtualCam(); + + void OnVirtualCamStart(); + void OnVirtualCamStop(int code); + + void SaveProjectDeferred(); + void SaveProject(); + + void SetTransition(OBSSource transition); + void OverrideTransition(OBSSource transition); + void TransitionToScene(OBSScene scene, bool force = false); + void TransitionToScene(OBSSource scene, bool force = false, bool quickTransition = false, int quickDuration = 0, + bool black = false, bool manual = false); + void SetCurrentScene(OBSSource scene, bool force = false); + + void UpdatePatronJson(const QString &text, const QString &error); + + void ShowContextBar(); + void HideContextBar(); + void PauseRecording(); + void UnpauseRecording(); + + void UpdateEditMenu(); + +private slots: + + void on_actionMainUndo_triggered(); + void on_actionMainRedo_triggered(); + + void AddSceneItem(OBSSceneItem item); + void AddScene(OBSSource source); + void RemoveScene(OBSSource source); + void RenameSources(OBSSource source, QString newName, QString prevName); + + void ActivateAudioSource(OBSSource source); + void DeactivateAudioSource(OBSSource source); + + void DuplicateSelectedScene(); + void RemoveSelectedScene(); + + void ToggleAlwaysOnTop(); + + void ReorderSources(OBSScene scene); + void RefreshSources(OBSScene scene); + + void ProcessHotkey(obs_hotkey_id id, bool pressed); + + void AddTransition(const char *id); + void RenameTransition(OBSSource transition); + void TransitionClicked(); + void TransitionStopped(); + void TransitionFullyStopped(); + void TriggerQuickTransition(int id); + + void SetDeinterlacingMode(); + void SetDeinterlacingOrder(); + + void SetScaleFilter(); + + void SetBlendingMethod(); + void SetBlendingMode(); + + void IconActivated(QSystemTrayIcon::ActivationReason reason); + void SetShowing(bool showing); + + void ToggleShowHide(); + + void HideAudioControl(); + void UnhideAllAudioControls(); + void ToggleHideMixer(); + + void MixerRenameSource(); + + void on_vMixerScrollArea_customContextMenuRequested(); + void on_hMixerScrollArea_customContextMenuRequested(); + + void on_actionCopySource_triggered(); + void on_actionPasteRef_triggered(); + void on_actionPasteDup_triggered(); + + void on_actionCopyFilters_triggered(); + void on_actionPasteFilters_triggered(); + void AudioMixerCopyFilters(); + void AudioMixerPasteFilters(); + void SourcePasteFilters(OBSSource source, OBSSource dstSource); + + void on_previewXScrollBar_valueChanged(int value); + void on_previewYScrollBar_valueChanged(int value); + + void PreviewScalingModeChanged(int value); + + void ColorChange(); + + SourceTreeItem *GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem); + + void on_actionShowAbout_triggered(); + + void EnablePreview(); + void DisablePreview(); + + void EnablePreviewProgram(); + void DisablePreviewProgram(); + + void SceneCopyFilters(); + void ScenePasteFilters(); + + void CheckDiskSpaceRemaining(); + void OpenSavedProjector(SavedProjectorInfo *info); + + void ResetStatsHotkey(); + + void SetImageIcon(const QIcon &icon); + void SetColorIcon(const QIcon &icon); + void SetSlideshowIcon(const QIcon &icon); + void SetAudioInputIcon(const QIcon &icon); + void SetAudioOutputIcon(const QIcon &icon); + void SetDesktopCapIcon(const QIcon &icon); + void SetWindowCapIcon(const QIcon &icon); + void SetGameCapIcon(const QIcon &icon); + void SetCameraIcon(const QIcon &icon); + void SetTextIcon(const QIcon &icon); + void SetMediaIcon(const QIcon &icon); + void SetBrowserIcon(const QIcon &icon); + void SetGroupIcon(const QIcon &icon); + void SetSceneIcon(const QIcon &icon); + void SetDefaultIcon(const QIcon &icon); + void SetAudioProcessOutputIcon(const QIcon &icon); + + void TBarChanged(int value); + void TBarReleased(); + + void LockVolumeControl(bool lock); + + void UpdateVirtualCamConfig(const VCamConfig &config); + void RestartVirtualCam(const VCamConfig &config); + void RestartingVirtualCam(); + +private: + /* OBS Callbacks */ + static void SceneReordered(void *data, calldata_t *params); + static void SceneRefreshed(void *data, calldata_t *params); + static void SceneItemAdded(void *data, calldata_t *params); + static void SourceCreated(void *data, calldata_t *params); + static void SourceRemoved(void *data, calldata_t *params); + static void SourceActivated(void *data, calldata_t *params); + static void SourceDeactivated(void *data, calldata_t *params); + static void SourceAudioActivated(void *data, calldata_t *params); + static void SourceAudioDeactivated(void *data, calldata_t *params); + static void SourceRenamed(void *data, calldata_t *params); + static void RenderMain(void *data, uint32_t cx, uint32_t cy); + + void ResizePreview(uint32_t cx, uint32_t cy); + + void AddSource(const char *id); + QMenu *CreateAddSourcePopupMenu(); + void AddSourcePopupMenu(const QPoint &pos); + void copyActionsDynamicProperties(); + + static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); + + void AutoRemux(QString input, bool no_show = false); + + void UpdateIsRecordingPausable(); + + bool IsFFmpegOutputToURL() const; + bool OutputPathValid(); + void OutputPathInvalidMessage(); + + bool LowDiskSpace(); + void DiskSpaceMessage(); + + OBSSource prevFTBSource = nullptr; + + float dpi = 1.0; + +public: + OBSSource GetProgramSource(); + OBSScene GetCurrentScene(); + + void SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n); + + inline OBSSource GetCurrentSceneSource() + { + OBSScene curScene = GetCurrentScene(); + return OBSSource(obs_scene_get_source(curScene)); + } + + obs_service_t *GetService(); + void SetService(obs_service_t *service); + + int GetTransitionDuration(); + int GetTbarPosition(); + + inline bool IsPreviewProgramMode() const { return os_atomic_load_bool(&previewProgramMode); } + + inline bool VCamEnabled() const { return vcamEnabled; } + + bool Active() const; + + void ResetUI(); + int ResetVideo(); + bool ResetAudio(); + + void ResetOutputs(); + + void RefreshVolumeColors(); + + void ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel); + + void NewProject(); + void LoadProject(); + + inline void GetDisplayRect(int &x, int &y, int &cx, int &cy) + { + x = previewX; + y = previewY; + cx = previewCX; + cy = previewCY; + } + + inline bool SavingDisabled() const { return disableSaving; } + + inline double GetCPUUsage() const { return os_cpu_usage_info_query(cpuUsageInfo); } + + void SaveService(); + bool LoadService(); + + inline Auth *GetAuth() { return auth.get(); } + + inline void EnableOutputs(bool enable) + { + if (enable) { + if (--disableOutputsRef < 0) + disableOutputsRef = 0; + } else { + disableOutputsRef++; + } + } + + QMenu *AddDeinterlacingMenu(QMenu *menu, obs_source_t *source); + QMenu *AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item); + QMenu *AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item); + void CreateSourcePopupMenu(int idx, bool preview); + + void UpdateTitleBar(); + + void SystemTrayInit(); + void SystemTray(bool firstStarted); + + void OpenSavedProjectors(); + + void CreateInteractionWindow(obs_source_t *source); + void CreatePropertiesWindow(obs_source_t *source); + void CreateFiltersWindow(obs_source_t *source); + void CreateEditTransformWindow(obs_sceneitem_t *item); + + QAction *AddDockWidget(QDockWidget *dock); + void AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser = false); + void RemoveDockWidget(const QString &name); + bool IsDockObjectNameUsed(const QString &name); + void AddCustomDockWidget(QDockWidget *dock); + + static OBSBasic *Get(); + + const char *GetCurrentOutputPath(); + + void DeleteProjector(OBSProjector *projector); + + static QList GetProjectorMenuMonitorsFormatted(); + template + static void AddProjectorMenuMonitors(QMenu *parent, Receiver *target, void (Receiver::*slot)(Args...)) + { + auto projectors = GetProjectorMenuMonitorsFormatted(); + for (int i = 0; i < projectors.size(); i++) { + QString str = projectors[i]; + QAction *action = parent->addAction(str, target, slot); + action->setProperty("monitor", i); + } + } + + QIcon GetSourceIcon(const char *id) const; + QIcon GetGroupIcon() const; + QIcon GetSceneIcon() const; + + OBSWeakSource copyFilter; + + void ShowStatusBarMessage(const QString &message); + + static OBSData BackupScene(obs_scene_t *scene, std::vector *sources = nullptr); + void CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data); + + static inline OBSData BackupScene(obs_source_t *scene_source, std::vector *sources = nullptr) + { + obs_scene_t *scene = obs_scene_from_source(scene_source); + return BackupScene(scene, sources); + } + + void CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array); + + void SetDisplayAffinity(QWindow *window); + + QColor GetSelectionColor() const; + inline bool Closing() { return closing; } + +protected: + virtual void closeEvent(QCloseEvent *event) override; + virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; + virtual void changeEvent(QEvent *event) override; + +private slots: + void on_actionFullscreenInterface_triggered(); + + void on_actionShow_Recordings_triggered(); + void on_actionRemux_triggered(); + void on_action_Settings_triggered(); + void on_actionShowMacPermissions_triggered(); + void on_actionShowMissingFiles_triggered(); + void on_actionAdvAudioProperties_triggered(); + void on_actionMixerToolbarAdvAudio_triggered(); + void on_actionMixerToolbarMenu_triggered(); + void on_actionShowLogs_triggered(); + void on_actionUploadCurrentLog_triggered(); + void on_actionUploadLastLog_triggered(); + void on_actionViewCurrentLog_triggered(); + void on_actionCheckForUpdates_triggered(); + void on_actionRepair_triggered(); + void on_actionShowWhatsNew_triggered(); + void on_actionRestartSafe_triggered(); + + void on_actionShowCrashLogs_triggered(); + void on_actionUploadLastCrashLog_triggered(); + + void on_actionEditTransform_triggered(); + void on_actionCopyTransform_triggered(); + void on_actionPasteTransform_triggered(); + void on_actionRotate90CW_triggered(); + void on_actionRotate90CCW_triggered(); + void on_actionRotate180_triggered(); + void on_actionFlipHorizontal_triggered(); + void on_actionFlipVertical_triggered(); + void on_actionFitToScreen_triggered(); + void on_actionStretchToScreen_triggered(); + void on_actionCenterToScreen_triggered(); + void on_actionVerticalCenter_triggered(); + void on_actionHorizontalCenter_triggered(); + void on_actionSceneFilters_triggered(); + + void on_OBSBasic_customContextMenuRequested(const QPoint &pos); + + void on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *prev); + void on_scenes_customContextMenuRequested(const QPoint &pos); + void GridActionClicked(); + void on_actionSceneListMode_triggered(); + void on_actionSceneGridMode_triggered(); + void on_actionAddScene_triggered(); + void on_actionRemoveScene_triggered(); + void on_actionSceneUp_triggered(); + void on_actionSceneDown_triggered(); + void on_sources_customContextMenuRequested(const QPoint &pos); + void on_scenes_itemDoubleClicked(QListWidgetItem *item); + void on_actionAddSource_triggered(); + void on_actionRemoveSource_triggered(); + void on_actionInteract_triggered(); + void on_actionSourceProperties_triggered(); + void on_actionSourceUp_triggered(); + void on_actionSourceDown_triggered(); + + void on_actionMoveUp_triggered(); + void on_actionMoveDown_triggered(); + void on_actionMoveToTop_triggered(); + void on_actionMoveToBottom_triggered(); + + void on_actionLockPreview_triggered(); + + void on_scalingMenu_aboutToShow(); + void on_actionScaleWindow_triggered(); + void on_actionScaleCanvas_triggered(); + void on_actionScaleOutput_triggered(); + + void Screenshot(OBSSource source_ = nullptr); + void ScreenshotSelectedSource(); + void ScreenshotProgram(); + void ScreenshotScene(); + + void on_actionHelpPortal_triggered(); + void on_actionWebsite_triggered(); + void on_actionDiscord_triggered(); + void on_actionReleaseNotes_triggered(); + + void on_preview_customContextMenuRequested(); + void ProgramViewContextMenuRequested(); + void on_previewDisabledWidget_customContextMenuRequested(); + + void on_actionShowSettingsFolder_triggered(); + void on_actionShowProfileFolder_triggered(); + + void on_actionAlwaysOnTop_triggered(); + + void on_toggleListboxToolbars_toggled(bool visible); + void on_toggleContextBar_toggled(bool visible); + void on_toggleStatusBar_toggled(bool visible); + void on_toggleSourceIcons_toggled(bool visible); + + void on_transitions_currentIndexChanged(int index); + void on_transitionAdd_clicked(); + void on_transitionRemove_clicked(); + void on_transitionProps_clicked(); + void on_transitionDuration_valueChanged(); + + void ShowTransitionProperties(); + void HideTransitionProperties(); + + // Source Context Buttons + void on_sourcePropertiesButton_clicked(); + void on_sourceFiltersButton_clicked(); + void on_sourceInteractButton_clicked(); + + void on_autoConfigure_triggered(); + void on_stats_triggered(); + + void on_resetUI_triggered(); + void on_resetDocks_triggered(bool force = false); + void on_lockDocks_toggled(bool lock); + void on_multiviewProjectorWindowed_triggered(); + void on_sideDocks_toggled(bool side); + + void logUploadFinished(const QString &text, const QString &error); + void crashUploadFinished(const QString &text, const QString &error); + void openLogDialog(const QString &text, const bool crash); + + void updateCheckFinished(); + + void MoveSceneToTop(); + void MoveSceneToBottom(); + + void EditSceneName(); + void EditSceneItemName(); + + void SceneNameEdited(QWidget *editor); + + void OpenSceneFilters(); + void OpenFilters(OBSSource source = nullptr); + void OpenProperties(OBSSource source = nullptr); + void OpenInteraction(OBSSource source = nullptr); + void OpenEditTransform(OBSSceneItem item = nullptr); + + void EnablePreviewDisplay(bool enable); + void TogglePreview(); + + void OpenStudioProgramProjector(); + void OpenPreviewProjector(); + void OpenSourceProjector(); + void OpenMultiviewProjector(); + void OpenSceneProjector(); + + void OpenStudioProgramWindow(); + void OpenPreviewWindow(); + void OpenSourceWindow(); + void OpenSceneWindow(); + + void StackedMixerAreaContextMenuRequested(); + + void ResizeOutputSizeOfSource(); + + void RepairOldExtraDockName(); + void RepairCustomExtraDockName(); + + /* Stream action (start/stop) slot */ + void StreamActionTriggered(); + + /* Record action (start/stop) slot */ + void RecordActionTriggered(); + + /* Record pause (pause/unpause) slot */ + void RecordPauseToggled(); + + /* Replay Buffer action (start/stop) slot */ + void ReplayBufferActionTriggered(); + + /* Virtual Cam action (start/stop) slots */ + void VirtualCamActionTriggered(); + + void OpenVirtualCamConfig(); + + /* Studio Mode toggle slot */ + void TogglePreviewProgramMode(); + +public slots: + void on_actionResetTransform_triggered(); + + bool StreamingActive(); + bool RecordingActive(); + bool ReplayBufferActive(); + bool VirtualCamActive(); + + void ClearContextBar(); + void UpdateContextBar(bool force = false); + void UpdateContextBarDeferred(bool force = false); + void UpdateContextBarVisibility(); + +signals: + /* Streaming signals */ + void StreamingPreparing(); + void StreamingStarting(bool broadcastAutoStart); + void StreamingStarted(bool withDelay = false); + void StreamingStopping(); + void StreamingStopped(bool withDelay = false); + + /* Broadcast Flow signals */ + void BroadcastFlowEnabled(bool enabled); + void BroadcastStreamReady(bool ready); + void BroadcastStreamActive(); + void BroadcastStreamStarted(bool autoStop); + + /* Recording signals */ + void RecordingStarted(bool pausable = false); + void RecordingPaused(); + void RecordingUnpaused(); + void RecordingStopping(); + void RecordingStopped(); + + /* Replay Buffer signals */ + void ReplayBufEnabled(bool enabled); + void ReplayBufStarted(); + void ReplayBufStopping(); + void ReplayBufStopped(); + + /* Virtual Camera signals */ + void VirtualCamEnabled(); + void VirtualCamStarted(); + void VirtualCamStopped(); + + /* Studio Mode signal */ + void PreviewProgramModeChanged(bool enabled); + void CanvasResized(uint32_t width, uint32_t height); + void OutputResized(uint32_t width, uint32_t height); + + /* Preview signals */ + void PreviewXScrollBarMoved(int value); + void PreviewYScrollBarMoved(int value); + +private: + std::unique_ptr ui; + + QPointer controlsDock; + +public: + /* `undo_s` needs to be declared after `ui` to prevent an uninitialized + * warning for `ui` while initializing `undo_s`. */ + undo_stack undo_s; + + explicit OBSBasic(QWidget *parent = 0); + virtual ~OBSBasic(); + + virtual void OBSInit() override; + + virtual config_t *Config() const override; + + virtual int GetProfilePath(char *path, size_t size, const char *file) const override; + + static void InitBrowserPanelSafeBlock(); +#ifdef YOUTUBE_ENABLED + void NewYouTubeAppDock(); + void DeleteYouTubeAppDock(); + YouTubeAppDock *GetYouTubeAppDock(); +#endif + // MARK: - Generic UI Helper Functions + OBSPromptResult PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback); + + // MARK: - OBS Profile Management +private: + OBSProfileCache profiles{}; + + void SetupNewProfile(const std::string &profileName, bool useWizard = false); + void SetupDuplicateProfile(const std::string &profileName); + void SetupRenameProfile(const std::string &profileName); + + const OBSProfile &CreateProfile(const std::string &profileName); + void RemoveProfile(OBSProfile profile); + + void ChangeProfile(); + + void RefreshProfileCache(); + + void RefreshProfiles(bool refreshCache = false); + + void ActivateProfile(const OBSProfile &profile, bool reset = false); + void UpdateProfileEncoders(); + std::vector GetRestartRequirements(const ConfigFile &config) const; + void ResetProfileData(); + void CheckForSimpleModeX264Fallback(); + +public: + inline const OBSProfileCache &GetProfileCache() const noexcept { return profiles; }; + + const OBSProfile &GetCurrentProfile() const; + + std::optional GetProfileByName(const std::string &profileName) const; + std::optional GetProfileByDirectoryName(const std::string &directoryName) const; + +private slots: + void on_actionNewProfile_triggered(); + void on_actionDupProfile_triggered(); + void on_actionRenameProfile_triggered(); + void on_actionRemoveProfile_triggered(bool skipConfirmation = false); + void on_actionImportProfile_triggered(); + void on_actionExportProfile_triggered(); + +public slots: + bool CreateNewProfile(const QString &name); + bool CreateDuplicateProfile(const QString &name); + void DeleteProfile(const QString &profileName); + + // MARK: - OBS Scene Collection Management +private: + OBSSceneCollectionCache collections{}; + + void SetupNewSceneCollection(const std::string &collectionName); + void SetupDuplicateSceneCollection(const std::string &collectionName); + void SetupRenameSceneCollection(const std::string &collectionName); + + const OBSSceneCollection &CreateSceneCollection(const std::string &collectionName); + void RemoveSceneCollection(OBSSceneCollection collection); + + bool CreateDuplicateSceneCollection(const QString &name); + void DeleteSceneCollection(const QString &name); + void ChangeSceneCollection(); + + void RefreshSceneCollectionCache(); + + void RefreshSceneCollections(bool refreshCache = false); + void ActivateSceneCollection(const OBSSceneCollection &collection); + +public: + inline const OBSSceneCollectionCache &GetSceneCollectionCache() const noexcept { return collections; }; + + const OBSSceneCollection &GetCurrentSceneCollection() const; + + std::optional GetSceneCollectionByName(const std::string &collectionName) const; + std::optional GetSceneCollectionByFileName(const std::string &fileName) const; + +private slots: + void on_actionNewSceneCollection_triggered(); + void on_actionDupSceneCollection_triggered(); + void on_actionRenameSceneCollection_triggered(); + void on_actionRemoveSceneCollection_triggered(bool skipConfirmation = false); + void on_actionImportSceneCollection_triggered(); + void on_actionExportSceneCollection_triggered(); + void on_actionRemigrateSceneCollection_triggered(); + +public slots: + bool CreateNewSceneCollection(const QString &name); +}; + +extern bool cef_js_avail; + +class SceneRenameDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + SceneRenameDelegate(QObject *parent); + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const override; + +protected: + virtual bool eventFilter(QObject *editor, QEvent *event) override; +}; diff --git a/UI/window-basic-status-bar.cpp b/frontend/widgets/OBSBasicStatusBar.cpp similarity index 100% rename from UI/window-basic-status-bar.cpp rename to frontend/widgets/OBSBasicStatusBar.cpp diff --git a/UI/window-basic-status-bar.hpp b/frontend/widgets/OBSBasicStatusBar.hpp similarity index 100% rename from UI/window-basic-status-bar.hpp rename to frontend/widgets/OBSBasicStatusBar.hpp diff --git a/UI/window-basic-main-browser.cpp b/frontend/widgets/OBSBasic_Browser.cpp similarity index 100% rename from UI/window-basic-main-browser.cpp rename to frontend/widgets/OBSBasic_Browser.cpp diff --git a/frontend/widgets/OBSBasic_Clipboard.cpp b/frontend/widgets/OBSBasic_Clipboard.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Clipboard.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_ContextToolbar.cpp b/frontend/widgets/OBSBasic_ContextToolbar.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_ContextToolbar.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Docks.cpp b/frontend/widgets/OBSBasic_Docks.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Docks.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/UI/window-basic-main-dropfiles.cpp b/frontend/widgets/OBSBasic_Dropfiles.cpp similarity index 100% rename from UI/window-basic-main-dropfiles.cpp rename to frontend/widgets/OBSBasic_Dropfiles.cpp diff --git a/frontend/widgets/OBSBasic_Hotkeys.cpp b/frontend/widgets/OBSBasic_Hotkeys.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Hotkeys.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/UI/window-basic-main-icons.cpp b/frontend/widgets/OBSBasic_Icons.cpp similarity index 100% rename from UI/window-basic-main-icons.cpp rename to frontend/widgets/OBSBasic_Icons.cpp diff --git a/frontend/widgets/OBSBasic_MainControls.cpp b/frontend/widgets/OBSBasic_MainControls.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_MainControls.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_OutputHandler.cpp b/frontend/widgets/OBSBasic_OutputHandler.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_OutputHandler.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Preview.cpp b/frontend/widgets/OBSBasic_Preview.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Preview.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/UI/window-basic-main-profiles.cpp b/frontend/widgets/OBSBasic_Profiles.cpp similarity index 100% rename from UI/window-basic-main-profiles.cpp rename to frontend/widgets/OBSBasic_Profiles.cpp diff --git a/frontend/widgets/OBSBasic_Projectors.cpp b/frontend/widgets/OBSBasic_Projectors.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Projectors.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Recording.cpp b/frontend/widgets/OBSBasic_Recording.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Recording.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_ReplayBuffer.cpp b/frontend/widgets/OBSBasic_ReplayBuffer.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_ReplayBuffer.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_SceneCollections.cpp b/frontend/widgets/OBSBasic_SceneCollections.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_SceneCollections.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_SceneItems.cpp b/frontend/widgets/OBSBasic_SceneItems.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_SceneItems.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Scenes.cpp b/frontend/widgets/OBSBasic_Scenes.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Scenes.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Screenshots.cpp b/frontend/widgets/OBSBasic_Screenshots.cpp new file mode 100644 index 000000000..a0b3622d1 --- /dev/null +++ b/frontend/widgets/OBSBasic_Screenshots.cpp @@ -0,0 +1,347 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "window-basic-main.hpp" +#include "screenshot-obj.hpp" + +#include + +#ifdef _WIN32 +#include +#include +#include +#pragma comment(lib, "windowscodecs.lib") +#endif + +static void ScreenshotTick(void *param, float); + +/* ========================================================================= */ + +ScreenshotObj::ScreenshotObj(obs_source_t *source) : weakSource(OBSGetWeakRef(source)) +{ + obs_add_tick_callback(ScreenshotTick, this); +} + +ScreenshotObj::~ScreenshotObj() +{ + obs_enter_graphics(); + gs_stagesurface_destroy(stagesurf); + gs_texrender_destroy(texrender); + obs_leave_graphics(); + + obs_remove_tick_callback(ScreenshotTick, this); + + if (th.joinable()) { + th.join(); + + if (cx && cy) { + OBSBasic *main = OBSBasic::Get(); + main->ShowStatusBarMessage( + QTStr("Basic.StatusBar.ScreenshotSavedTo").arg(QT_UTF8(path.c_str()))); + + main->lastScreenshot = path; + + main->OnEvent(OBS_FRONTEND_EVENT_SCREENSHOT_TAKEN); + } + } +} + +void ScreenshotObj::Screenshot() +{ + OBSSource source = OBSGetStrongRef(weakSource); + + if (source) { + cx = obs_source_get_width(source); + cy = obs_source_get_height(source); + } else { + obs_video_info ovi; + obs_get_video_info(&ovi); + cx = ovi.base_width; + cy = ovi.base_height; + } + + if (!cx || !cy) { + blog(LOG_WARNING, "Cannot screenshot, invalid target size"); + obs_remove_tick_callback(ScreenshotTick, this); + deleteLater(); + return; + } + +#ifdef _WIN32 + enum gs_color_space space = obs_source_get_color_space(source, 0, nullptr); + if (space == GS_CS_709_EXTENDED) { + /* Convert for JXR */ + space = GS_CS_709_SCRGB; + } +#else + /* Tonemap to SDR if HDR */ + const enum gs_color_space space = GS_CS_SRGB; +#endif + const enum gs_color_format format = gs_get_format_from_space(space); + + texrender = gs_texrender_create(format, GS_ZS_NONE); + stagesurf = gs_stagesurface_create(cx, cy, format); + + if (gs_texrender_begin_with_color_space(texrender, cx, cy, space)) { + vec4 zero; + vec4_zero(&zero); + + gs_clear(GS_CLEAR_COLOR, &zero, 0.0f, 0); + gs_ortho(0.0f, (float)cx, 0.0f, (float)cy, -100.0f, 100.0f); + + gs_blend_state_push(); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + + if (source) { + obs_source_inc_showing(source); + obs_source_video_render(source); + obs_source_dec_showing(source); + } else { + obs_render_main_texture(); + } + + gs_blend_state_pop(); + gs_texrender_end(texrender); + } +} + +void ScreenshotObj::Download() +{ + gs_stage_texture(stagesurf, gs_texrender_get_texture(texrender)); +} + +void ScreenshotObj::Copy() +{ + uint8_t *videoData = nullptr; + uint32_t videoLinesize = 0; + + if (gs_stagesurface_map(stagesurf, &videoData, &videoLinesize)) { + if (gs_stagesurface_get_color_format(stagesurf) == GS_RGBA16F) { + const uint32_t linesize = cx * 8; + half_bytes.reserve(cx * cy * 8); + + for (uint32_t y = 0; y < cy; y++) { + const uint8_t *const line = videoData + (y * videoLinesize); + half_bytes.insert(half_bytes.end(), line, line + linesize); + } + } else { + image = QImage(cx, cy, QImage::Format::Format_RGBX8888); + + int linesize = image.bytesPerLine(); + for (int y = 0; y < (int)cy; y++) + memcpy(image.scanLine(y), videoData + (y * videoLinesize), linesize); + } + + gs_stagesurface_unmap(stagesurf); + } +} + +void ScreenshotObj::Save() +{ + OBSBasic *main = OBSBasic::Get(); + config_t *config = main->Config(); + + const char *mode = config_get_string(config, "Output", "Mode"); + const char *type = config_get_string(config, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") ? config_get_string(config, "AdvOut", "FFFilePath") + : config_get_string(config, "AdvOut", "RecFilePath"); + const char *rec_path = strcmp(mode, "Advanced") ? config_get_string(config, "SimpleOutput", "FilePath") + : adv_path; + + bool noSpace = config_get_bool(config, "SimpleOutput", "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(config, "Output", "FilenameFormatting"); + bool overwriteIfExists = config_get_bool(config, "Output", "OverwriteIfExists"); + + const char *ext = half_bytes.empty() ? "png" : "jxr"; + path = GetOutputFilename(rec_path, ext, noSpace, overwriteIfExists, + GetFormatString(filenameFormat, "Screenshot", nullptr).c_str()); + + th = std::thread([this] { MuxAndFinish(); }); +} + +#ifdef _WIN32 +static HRESULT SaveJxrImage(LPCWSTR path, uint8_t *pixels, uint32_t cx, uint32_t cy, IWICBitmapFrameEncode *frameEncode, + IPropertyBag2 *options) +{ + wchar_t lossless[] = L"Lossless"; + PROPBAG2 bag = {}; + bag.pstrName = lossless; + VARIANT value = {}; + value.vt = VT_BOOL; + value.bVal = TRUE; + HRESULT hr = options->Write(1, &bag, &value); + if (FAILED(hr)) + return hr; + + hr = frameEncode->Initialize(options); + if (FAILED(hr)) + return hr; + + hr = frameEncode->SetSize(cx, cy); + if (FAILED(hr)) + return hr; + + hr = frameEncode->SetResolution(72, 72); + if (FAILED(hr)) + return hr; + + WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat64bppRGBAHalf; + hr = frameEncode->SetPixelFormat(&pixelFormat); + if (FAILED(hr)) + return hr; + + if (memcmp(&pixelFormat, &GUID_WICPixelFormat64bppRGBAHalf, sizeof(WICPixelFormatGUID)) != 0) + return E_FAIL; + + hr = frameEncode->WritePixels(cy, cx * 8, cx * cy * 8, pixels); + if (FAILED(hr)) + return hr; + + hr = frameEncode->Commit(); + if (FAILED(hr)) + return hr; + + return S_OK; +} + +static HRESULT SaveJxr(LPCWSTR path, uint8_t *pixels, uint32_t cx, uint32_t cy) +{ + Microsoft::WRL::ComPtr factory; + HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(factory.GetAddressOf())); + if (FAILED(hr)) + return hr; + + Microsoft::WRL::ComPtr stream; + hr = factory->CreateStream(stream.GetAddressOf()); + if (FAILED(hr)) + return hr; + + hr = stream->InitializeFromFilename(path, GENERIC_WRITE); + if (FAILED(hr)) + return hr; + + Microsoft::WRL::ComPtr encoder = NULL; + hr = factory->CreateEncoder(GUID_ContainerFormatWmp, NULL, encoder.GetAddressOf()); + if (FAILED(hr)) + return hr; + + hr = encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache); + if (FAILED(hr)) + return hr; + + Microsoft::WRL::ComPtr frameEncode; + Microsoft::WRL::ComPtr options; + hr = encoder->CreateNewFrame(frameEncode.GetAddressOf(), options.GetAddressOf()); + if (FAILED(hr)) + return hr; + + hr = SaveJxrImage(path, pixels, cx, cy, frameEncode.Get(), options.Get()); + if (FAILED(hr)) + return hr; + + encoder->Commit(); + return S_OK; +} +#endif // #ifdef _WIN32 + +void ScreenshotObj::MuxAndFinish() +{ + if (half_bytes.empty()) { + image.save(QT_UTF8(path.c_str())); + blog(LOG_INFO, "Saved screenshot to '%s'", path.c_str()); + } else { +#ifdef _WIN32 + wchar_t *path_w = nullptr; + os_utf8_to_wcs_ptr(path.c_str(), 0, &path_w); + if (path_w) { + SaveJxr(path_w, half_bytes.data(), cx, cy); + bfree(path_w); + } +#endif // #ifdef _WIN32 + } + + deleteLater(); +} + +/* ========================================================================= */ + +#define STAGE_SCREENSHOT 0 +#define STAGE_DOWNLOAD 1 +#define STAGE_COPY_AND_SAVE 2 +#define STAGE_FINISH 3 + +static void ScreenshotTick(void *param, float) +{ + ScreenshotObj *data = reinterpret_cast(param); + + if (data->stage == STAGE_FINISH) { + return; + } + + obs_enter_graphics(); + + switch (data->stage) { + case STAGE_SCREENSHOT: + data->Screenshot(); + break; + case STAGE_DOWNLOAD: + data->Download(); + break; + case STAGE_COPY_AND_SAVE: + data->Copy(); + QMetaObject::invokeMethod(data, "Save"); + obs_remove_tick_callback(ScreenshotTick, data); + break; + } + + obs_leave_graphics(); + + data->stage++; +} + +void OBSBasic::Screenshot(OBSSource source) +{ + if (!!screenshotData) { + blog(LOG_WARNING, "Cannot take new screenshot, " + "screenshot currently in progress"); + return; + } + + screenshotData = new ScreenshotObj(source); +} + +void OBSBasic::ScreenshotSelectedSource() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (item) { + Screenshot(obs_sceneitem_get_source(item)); + } else { + blog(LOG_INFO, "Could not take a source screenshot: " + "no source selected"); + } +} + +void OBSBasic::ScreenshotProgram() +{ + Screenshot(GetProgramSource()); +} + +void OBSBasic::ScreenshotScene() +{ + Screenshot(GetCurrentSceneSource()); +} diff --git a/frontend/widgets/OBSBasic_Service.cpp b/frontend/widgets/OBSBasic_Service.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Service.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_StatusBar.cpp b/frontend/widgets/OBSBasic_StatusBar.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_StatusBar.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Streaming.cpp b/frontend/widgets/OBSBasic_Streaming.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Streaming.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_StudioMode.cpp b/frontend/widgets/OBSBasic_StudioMode.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_StudioMode.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_SysTray.cpp b/frontend/widgets/OBSBasic_SysTray.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_SysTray.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Transitions.cpp b/frontend/widgets/OBSBasic_Transitions.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Transitions.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_Updater.cpp b/frontend/widgets/OBSBasic_Updater.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_Updater.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_VirtualCam.cpp b/frontend/widgets/OBSBasic_VirtualCam.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_VirtualCam.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_VolControl.cpp b/frontend/widgets/OBSBasic_VolControl.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_VolControl.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSBasic_YouTube.cpp b/frontend/widgets/OBSBasic_YouTube.cpp new file mode 100644 index 000000000..47cac8dcc --- /dev/null +++ b/frontend/widgets/OBSBasic_YouTube.cpp @@ -0,0 +1,10203 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "ui-config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "platform.hpp" +#include "visibility-item-widget.hpp" +#include "item-widget-helpers.hpp" +#include "basic-controls.hpp" +#include "window-basic-settings.hpp" +#include "window-namedialog.hpp" +#include "window-basic-auto-config.hpp" +#include "window-basic-source-select.hpp" +#include "window-basic-main.hpp" +#include "window-basic-stats.hpp" +#include "window-basic-main-outputs.hpp" +#include "window-basic-vcam-config.hpp" +#include "window-log-reply.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-projector.hpp" +#include "window-remux.hpp" +#ifdef YOUTUBE_ENABLED +#include "auth-youtube.hpp" +#include "window-youtube-actions.hpp" +#include "youtube-api-wrappers.hpp" +#endif +#include "window-whats-new.hpp" +#include "context-bar-controls.hpp" +#include "obs-proxy-style.hpp" +#include "display-helpers.hpp" +#include "volume-control.hpp" +#include "remote-text.hpp" +#include "ui-validation.hpp" +#include "media-controls.hpp" +#include "undo-stack-obs.hpp" +#include +#include + +#ifdef _WIN32 +#include "update/win-update.hpp" +#include "update/shared-update.hpp" +#include "windows.h" +#endif + +#ifdef WHATSNEW_ENABLED +#include "update/models/whatsnew.hpp" +#endif + +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "update/shared-update.hpp" +#endif + +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + +#include "ui_OBSBasic.h" +#include "ui_ColorSelect.h" + +#include + +#ifdef ENABLE_WAYLAND +#include +#endif + +using namespace std; + +#ifdef BROWSER_AVAILABLE +#include +#endif + +#include "ui-config.h" + +struct QCef; +struct QCefCookieManager; + +QCef *cef = nullptr; +QCefCookieManager *panel_cookies = nullptr; +bool cef_js_avail = false; + +extern std::string opt_starting_profile; +extern std::string opt_starting_collection; + +void DestroyPanelCookieManager(); + +namespace { + +QPointer obsWhatsNew; + +template struct SignalContainer { + OBSRef ref; + vector> handlers; +}; +} // namespace + +extern volatile long insideEventLoop; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSceneItem); +Q_DECLARE_METATYPE(OBSSource); +Q_DECLARE_METATYPE(obs_order_movement); +Q_DECLARE_METATYPE(SignalContainer); + +QDataStream &operator<<(QDataStream &out, const SignalContainer &v) +{ + out << v.ref; + return out; +} + +QDataStream &operator>>(QDataStream &in, SignalContainer &v) +{ + in >> v.ref; + return in; +} + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +template static void SetOBSRef(QListWidgetItem *item, T &&val) +{ + item->setData(static_cast(QtDataRole::OBSRef), QVariant::fromValue(val)); +} + +static void AddExtraModulePaths() +{ + string plugins_path, plugins_data_path; + char *s; + + s = getenv("OBS_PLUGINS_PATH"); + if (s) + plugins_path = s; + + s = getenv("OBS_PLUGINS_DATA_PATH"); + if (s) + plugins_data_path = s; + + if (!plugins_path.empty() && !plugins_data_path.empty()) { +#if defined(__APPLE__) + plugins_path += "/%module%.plugin/Contents/MacOS"; + plugins_data_path += "/%module%.plugin/Contents/Resources"; + obs_add_module_path(plugins_path.c_str(), plugins_data_path.c_str()); +#else + string data_path_with_module_suffix; + data_path_with_module_suffix += plugins_data_path; + data_path_with_module_suffix += "/%module%"; + obs_add_module_path(plugins_path.c_str(), data_path_with_module_suffix.c_str()); +#endif + } + + if (portable_mode) + return; + + char base_module_dir[512]; +#if defined(_WIN32) + int ret = GetProgramDataPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#elif defined(__APPLE__) + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%.plugin"); +#else + int ret = GetAppConfigPath(base_module_dir, sizeof(base_module_dir), "obs-studio/plugins/%module%"); +#endif + + if (ret <= 0) + return; + + string path = base_module_dir; +#if defined(__APPLE__) + /* User Application Support Search Path */ + obs_add_module_path((path + "/Contents/MacOS").c_str(), (path + "/Contents/Resources").c_str()); + +#ifndef __aarch64__ + /* Legacy System Library Search Path */ + char system_legacy_module_dir[PATH_MAX]; + GetProgramDataPath(system_legacy_module_dir, sizeof(system_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_system_legacy = system_legacy_module_dir; + obs_add_module_path((path_system_legacy + "/bin").c_str(), (path_system_legacy + "/data").c_str()); + + /* Legacy User Application Support Search Path */ + char user_legacy_module_dir[PATH_MAX]; + GetAppConfigPath(user_legacy_module_dir, sizeof(user_legacy_module_dir), "obs-studio/plugins/%module%"); + std::string path_user_legacy = user_legacy_module_dir; + obs_add_module_path((path_user_legacy + "/bin").c_str(), (path_user_legacy + "/data").c_str()); +#endif +#else +#if ARCH_BITS == 64 + obs_add_module_path((path + "/bin/64bit").c_str(), (path + "/data").c_str()); +#else + obs_add_module_path((path + "/bin/32bit").c_str(), (path + "/data").c_str()); +#endif +#endif +} + +/* First-party modules considered to be potentially unsafe to load in Safe Mode + * due to them allowing external code (e.g. scripts) to modify OBS's state. */ +static const unordered_set unsafe_modules = { + "frontend-tools", // Scripting + "obs-websocket", // Allows outside modifications +}; + +static void SetSafeModuleNames() +{ +#ifndef SAFE_MODULES + return; +#else + string module; + stringstream modules(SAFE_MODULES); + + while (getline(modules, module, '|')) { + /* When only disallowing third-party plugins, still add + * "unsafe" bundled modules to the safe list. */ + if (disable_3p_plugins || !unsafe_modules.count(module)) + obs_add_safe_module(module.c_str()); + } +#endif +} + +extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main); + +void assignDockToggle(QDockWidget *dock, QAction *action) +{ + auto handleWindowToggle = [action](bool vis) { + action->blockSignals(true); + action->setChecked(vis); + action->blockSignals(false); + }; + auto handleMenuToggle = [dock](bool check) { + dock->blockSignals(true); + dock->setVisible(check); + dock->blockSignals(false); + }; + + dock->connect(dock->toggleViewAction(), &QAction::toggled, handleWindowToggle); + dock->connect(action, &QAction::toggled, handleMenuToggle); +} + +void setupDockAction(QDockWidget *dock) +{ + QAction *action = dock->toggleViewAction(); + + auto neverDisable = [action]() { + QSignalBlocker block(action); + action->setEnabled(true); + }; + + auto newToggleView = [dock](bool check) { + QSignalBlocker block(dock); + dock->setVisible(check); + }; + + // Replace the slot connected by default + QObject::disconnect(action, &QAction::triggered, nullptr, 0); + dock->connect(action, &QAction::triggered, newToggleView); + + // Make the action unable to be disabled + action->connect(action, &QAction::enabledChanged, neverDisable); +} + +extern void RegisterTwitchAuth(); +extern void RegisterRestreamAuth(); +#ifdef YOUTUBE_ENABLED +extern void RegisterYoutubeAuth(); +#endif + +OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic) +{ + setAttribute(Qt::WA_NativeWindow); + +#ifdef TWITCH_ENABLED + RegisterTwitchAuth(); +#endif +#ifdef RESTREAM_ENABLED + RegisterRestreamAuth(); +#endif +#ifdef YOUTUBE_ENABLED + RegisterYoutubeAuth(); +#endif + + setAcceptDrops(true); + + setContextMenuPolicy(Qt::CustomContextMenu); + + QEvent::registerEventType(QEvent::User + QEvent::Close); + + api = InitializeAPIInterface(this); + + ui->setupUi(this); + ui->previewDisabledWidget->setVisible(false); + + /* Set up streaming connections */ + connect( + this, &OBSBasic::StreamingStarting, this, [this] { this->streamingStarting = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStarted, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::StreamingStopped, this, [this] { this->streamingStarting = false; }, + Qt::DirectConnection); + + /* Set up recording connections */ + connect( + this, &OBSBasic::RecordingStarted, this, + [this]() { + this->recordingStarted = true; + this->recordingPaused = false; + }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingPaused, this, [this]() { this->recordingPaused = true; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingUnpaused, this, [this]() { this->recordingPaused = false; }, + Qt::DirectConnection); + connect( + this, &OBSBasic::RecordingStopped, this, + [this]() { + this->recordingStarted = false; + this->recordingPaused = false; + }, + Qt::DirectConnection); + + /* Add controls dock */ + OBSBasicControls *controls = new OBSBasicControls(this); + controlsDock = new OBSDock(this); + controlsDock->setObjectName(QString::fromUtf8("controlsDock")); + controlsDock->setWindowTitle(QTStr("Basic.Main.Controls")); + /* Parenting is done there so controls will be deleted alongside controlsDock */ + controlsDock->setWidget(controls); + addDockWidget(Qt::BottomDockWidgetArea, controlsDock); + + connect(controls, &OBSBasicControls::StreamButtonClicked, this, &OBSBasic::StreamActionTriggered); + + connect(controls, &OBSBasicControls::StartStreamMenuActionClicked, this, &OBSBasic::StartStreaming); + connect(controls, &OBSBasicControls::StopStreamMenuActionClicked, this, &OBSBasic::StopStreaming); + connect(controls, &OBSBasicControls::ForceStopStreamMenuActionClicked, this, &OBSBasic::ForceStopStreaming); + + connect(controls, &OBSBasicControls::BroadcastButtonClicked, this, &OBSBasic::BroadcastButtonClicked); + + connect(controls, &OBSBasicControls::RecordButtonClicked, this, &OBSBasic::RecordActionTriggered); + connect(controls, &OBSBasicControls::PauseRecordButtonClicked, this, &OBSBasic::RecordPauseToggled); + + connect(controls, &OBSBasicControls::ReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferActionTriggered); + connect(controls, &OBSBasicControls::SaveReplayBufferButtonClicked, this, &OBSBasic::ReplayBufferSave); + + connect(controls, &OBSBasicControls::VirtualCamButtonClicked, this, &OBSBasic::VirtualCamActionTriggered); + connect(controls, &OBSBasicControls::VirtualCamConfigButtonClicked, this, &OBSBasic::OpenVirtualCamConfig); + + connect(controls, &OBSBasicControls::StudioModeButtonClicked, this, &OBSBasic::TogglePreviewProgramMode); + + connect(controls, &OBSBasicControls::SettingsButtonClicked, this, &OBSBasic::on_action_Settings_triggered); + + connect(controls, &OBSBasicControls::ExitButtonClicked, this, &QMainWindow::close); + + startingDockLayout = saveState(); + + statsDock = new OBSDock(); + statsDock->setObjectName(QStringLiteral("statsDock")); + statsDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + statsDock->setWindowTitle(QTStr("Basic.Stats")); + addDockWidget(Qt::BottomDockWidgetArea, statsDock); + statsDock->setVisible(false); + statsDock->setFloating(true); + statsDock->resize(700, 200); + + copyActionsDynamicProperties(); + + qRegisterMetaType("int64_t"); + qRegisterMetaType("uint32_t"); + qRegisterMetaType("OBSScene"); + qRegisterMetaType("OBSSceneItem"); + qRegisterMetaType("OBSSource"); + qRegisterMetaType("obs_hotkey_id"); + qRegisterMetaType("SavedProjectorInfo *"); + + ui->scenes->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->sources->setAttribute(Qt::WA_MacShowFocusRect, false); + + bool sceneGrid = config_get_bool(App()->GetUserConfig(), "BasicWindow", "gridMode"); + ui->scenes->SetGridMode(sceneGrid); + + if (sceneGrid) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + ui->scenes->setItemDelegate(new SceneRenameDelegate(ui->scenes)); + + auto displayResize = [this]() { + struct obs_video_info ovi; + + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + + UpdateContextBarVisibility(); + UpdatePreviewScrollbars(); + dpi = devicePixelRatioF(); + }; + dpi = devicePixelRatioF(); + + connect(windowHandle(), &QWindow::screenChanged, displayResize); + connect(ui->preview, &OBSQTDisplay::DisplayResized, displayResize); + + /* TODO: Move these into window-basic-preview */ + /* Preview Scaling label */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalePercent, + &OBSPreviewScalingLabel::PreviewScaleChanged); + + /* Preview Scaling dropdown */ + connect(ui->preview, &OBSBasicPreview::scalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewScaleChanged); + + connect(ui->preview, &OBSBasicPreview::fixedScalingChanged, ui->previewScalingMode, + &OBSPreviewScalingComboBox::PreviewFixedScalingChanged); + + connect(ui->previewScalingMode, &OBSPreviewScalingComboBox::currentIndexChanged, this, + &OBSBasic::PreviewScalingModeChanged); + + connect(this, &OBSBasic::CanvasResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::CanvasResized); + connect(this, &OBSBasic::OutputResized, ui->previewScalingMode, &OBSPreviewScalingComboBox::OutputResized); + + delete shortcutFilter; + shortcutFilter = CreateShortcutFilter(); + installEventFilter(shortcutFilter); + + stringstream name; + name << "OBS " << App()->GetVersionString(); + blog(LOG_INFO, "%s", name.str().c_str()); + blog(LOG_INFO, "---------------------------------"); + + UpdateTitleBar(); + + connect(ui->scenes->itemDelegate(), &QAbstractItemDelegate::closeEditor, this, &OBSBasic::SceneNameEdited); + + cpuUsageInfo = os_cpu_usage_info_start(); + cpuUsageTimer = new QTimer(this); + connect(cpuUsageTimer.data(), &QTimer::timeout, ui->statusbar, &OBSBasicStatusBar::UpdateCPUUsage); + cpuUsageTimer->start(3000); + + diskFullTimer = new QTimer(this); + connect(diskFullTimer, &QTimer::timeout, this, &OBSBasic::CheckDiskSpaceRemaining); + + renameScene = new QAction(QTStr("Rename"), ui->scenesDock); + renameScene->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameScene, &QAction::triggered, this, &OBSBasic::EditSceneName); + ui->scenesDock->addAction(renameScene); + + renameSource = new QAction(QTStr("Rename"), ui->sourcesDock); + renameSource->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameSource, &QAction::triggered, this, &OBSBasic::EditSceneItemName); + ui->sourcesDock->addAction(renameSource); + +#ifdef __APPLE__ + renameScene->setShortcut({Qt::Key_Return}); + renameSource->setShortcut({Qt::Key_Return}); + + ui->actionRemoveSource->setShortcuts({Qt::Key_Backspace}); + ui->actionRemoveScene->setShortcuts({Qt::Key_Backspace}); + + ui->actionCheckForUpdates->setMenuRole(QAction::AboutQtRole); + ui->action_Settings->setMenuRole(QAction::PreferencesRole); + ui->actionShowMacPermissions->setMenuRole(QAction::ApplicationSpecificRole); + ui->actionE_xit->setMenuRole(QAction::QuitRole); +#else + renameScene->setShortcut({Qt::Key_F2}); + renameSource->setShortcut({Qt::Key_F2}); +#endif + +#ifdef __linux__ + ui->actionE_xit->setShortcut(Qt::CTRL | Qt::Key_Q); +#endif + + auto addNudge = [this](const QKeySequence &seq, MoveDir direction, int distance) { + QAction *nudge = new QAction(ui->preview); + nudge->setShortcut(seq); + nudge->setShortcutContext(Qt::WidgetShortcut); + ui->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); + + /* Setup dock toggle action + * And hide all docks before restoring parent geometry */ +#define SETUP_DOCK(dock) \ + setupDockAction(dock); \ + ui->menuDocks->addAction(dock->toggleViewAction()); \ + dock->setVisible(false); + + SETUP_DOCK(ui->scenesDock); + SETUP_DOCK(ui->sourcesDock); + SETUP_DOCK(ui->mixerDock); + SETUP_DOCK(ui->transitionsDock); + SETUP_DOCK(controlsDock); + SETUP_DOCK(statsDock); +#undef SETUP_DOCK + + // Register shortcuts for Undo/Redo + ui->actionMainUndo->setShortcut(Qt::CTRL | Qt::Key_Z); + QList shrt; + shrt << QKeySequence((Qt::CTRL | Qt::SHIFT) | Qt::Key_Z) << QKeySequence(Qt::CTRL | Qt::Key_Y); + ui->actionMainRedo->setShortcuts(shrt); + + ui->actionMainUndo->setShortcutContext(Qt::ApplicationShortcut); + ui->actionMainRedo->setShortcutContext(Qt::ApplicationShortcut); + + QPoint curPos; + + //restore parent window geometry + const char *geometry = config_get_string(App()->GetUserConfig(), "BasicWindow", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + curPos = pos(); + } else { + QRect desktopRect = QGuiApplication::primaryScreen()->geometry(); + QSize adjSize = desktopRect.size() / 2 - size() / 2; + curPos = QPoint(adjSize.width(), adjSize.height()); + } + + QPoint curSize(width(), height()); + + QPoint statsDockSize(statsDock->width(), statsDock->height()); + QPoint statsDockPos = curSize / 2 - statsDockSize / 2; + QPoint newPos = curPos + statsDockPos; + statsDock->move(newPos); + +#ifdef HAVE_OBSCONFIG_H + ui->actionReleaseNotes->setVisible(true); +#endif + + ui->previewDisabledWidget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->enablePreviewButton, &QPushButton::clicked, this, &OBSBasic::TogglePreview); + + connect(ui->scenes, &SceneTree::scenesReordered, []() { OBSProjector::UpdateMultiviewProjectors(); }); + + connect(App(), &OBSApp::StyleChanged, this, [this]() { OnEvent(OBS_FRONTEND_EVENT_THEME_CHANGED); }); + + QActionGroup *actionGroup = new QActionGroup(this); + actionGroup->addAction(ui->actionSceneListMode); + actionGroup->addAction(ui->actionSceneGridMode); + + UpdatePreviewSafeAreas(); + UpdatePreviewSpacingHelpers(); + UpdatePreviewOverflowSettings(); +} + +static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent, vector &audioSources) +{ + OBSSourceAutoRelease source = obs_get_output_source(channel); + if (!source) + return; + + audioSources.push_back(source.Get()); + + OBSDataAutoRelease data = obs_save_source(source); + + obs_data_set_obj(parent, name, data); +} + +static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder, obs_data_array_t *quickTransitionData, + int transitionDuration, obs_data_array_t *transitions, OBSScene &scene, + OBSSource &curProgramScene, obs_data_array_t *savedProjectorList) +{ + obs_data_t *saveData = obs_data_create(); + + vector audioSources; + audioSources.reserve(6); + + SaveAudioDevice(DESKTOP_AUDIO_1, 1, saveData, audioSources); + SaveAudioDevice(DESKTOP_AUDIO_2, 2, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_1, 3, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_2, 4, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_3, 5, saveData, audioSources); + SaveAudioDevice(AUX_AUDIO_4, 6, saveData, audioSources); + + /* -------------------------------- */ + /* save non-group sources */ + + auto FilterAudioSources = [&](obs_source_t *source) { + if (obs_source_is_group(source)) + return false; + + return find(begin(audioSources), end(audioSources), source) == end(audioSources); + }; + using FilterAudioSources_t = decltype(FilterAudioSources); + + obs_data_array_t *sourcesArray = obs_save_sources_filtered( + [](void *data, obs_source_t *source) { + auto &func = *static_cast(data); + return func(source); + }, + static_cast(&FilterAudioSources)); + + /* -------------------------------- */ + /* save group sources separately */ + + /* saving separately ensures they won't be loaded in older versions */ + obs_data_array_t *groupsArray = obs_save_sources_filtered( + [](void *, obs_source_t *source) { return obs_source_is_group(source); }, nullptr); + + /* -------------------------------- */ + + OBSSourceAutoRelease transition = obs_get_output_source(0); + obs_source_t *currentScene = obs_scene_get_source(scene); + const char *sceneName = obs_source_get_name(currentScene); + const char *programName = obs_source_get_name(curProgramScene); + + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_string(saveData, "current_scene", sceneName); + obs_data_set_string(saveData, "current_program_scene", programName); + obs_data_set_array(saveData, "scene_order", sceneOrder); + obs_data_set_string(saveData, "name", sceneCollection); + obs_data_set_array(saveData, "sources", sourcesArray); + obs_data_set_array(saveData, "groups", groupsArray); + obs_data_set_array(saveData, "quick_transitions", quickTransitionData); + obs_data_set_array(saveData, "transitions", transitions); + obs_data_set_array(saveData, "saved_projectors", savedProjectorList); + obs_data_array_release(sourcesArray); + obs_data_array_release(groupsArray); + + obs_data_set_string(saveData, "current_transition", obs_source_get_name(transition)); + obs_data_set_int(saveData, "transition_duration", transitionDuration); + + return saveData; +} + +void OBSBasic::copyActionsDynamicProperties() +{ + // Themes need the QAction dynamic properties + for (QAction *x : ui->scenesToolbar->actions()) { + QWidget *temp = ui->scenesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->sourcesToolbar->actions()) { + QWidget *temp = ui->sourcesToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } + + for (QAction *x : ui->mixerToolbar->actions()) { + QWidget *temp = ui->mixerToolbar->widgetForAction(x); + + if (!temp) + continue; + + for (QByteArray &y : x->dynamicPropertyNames()) { + temp->setProperty(y, x->property(y)); + } + } +} + +void OBSBasic::UpdateVolumeControlsDecayRate() +{ + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->SetMeterDecayRate(meterDecayRate); + } +} + +void OBSBasic::UpdateVolumeControlsPeakMeterType() +{ + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + for (size_t i = 0; i < volumes.size(); i++) { + volumes[i]->setPeakMeterType(peakMeterType); + } +} + +void OBSBasic::ClearVolumeControls() +{ + for (VolControl *vol : volumes) + delete vol; + + volumes.clear(); +} + +void OBSBasic::RefreshVolumeColors() +{ + for (VolControl *vol : volumes) { + vol->refreshColors(); + } +} + +obs_data_array_t *OBSBasic::SaveSceneListOrder() +{ + obs_data_array_t *sceneOrder = obs_data_array_create(); + + for (int i = 0; i < ui->scenes->count(); i++) { + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "name", QT_TO_UTF8(ui->scenes->item(i)->text())); + obs_data_array_push_back(sceneOrder, data); + } + + return sceneOrder; +} + +obs_data_array_t *OBSBasic::SaveProjectors() +{ + obs_data_array_t *savedProjectors = obs_data_array_create(); + + auto saveProjector = [savedProjectors](OBSProjector *projector) { + if (!projector) + return; + + OBSDataAutoRelease data = obs_data_create(); + ProjectorType type = projector->GetProjectorType(); + + switch (type) { + case ProjectorType::Scene: + case ProjectorType::Source: { + OBSSource source = projector->GetSource(); + const char *name = obs_source_get_name(source); + obs_data_set_string(data, "name", name); + break; + } + default: + break; + } + + obs_data_set_int(data, "monitor", projector->GetMonitor()); + obs_data_set_int(data, "type", static_cast(type)); + obs_data_set_string(data, "geometry", projector->saveGeometry().toBase64().constData()); + + if (projector->IsAlwaysOnTopOverridden()) + obs_data_set_bool(data, "alwaysOnTop", projector->IsAlwaysOnTop()); + + obs_data_set_bool(data, "alwaysOnTopOverridden", projector->IsAlwaysOnTopOverridden()); + + obs_data_array_push_back(savedProjectors, data); + }; + + for (size_t i = 0; i < projectors.size(); i++) + saveProjector(static_cast(projectors[i])); + + return savedProjectors; +} + +void OBSBasic::Save(const char *file) +{ + OBSScene scene = GetCurrentScene(); + OBSSource curProgramScene = OBSGetStrongRef(programScene); + if (!curProgramScene) + curProgramScene = obs_scene_get_source(scene); + + OBSDataArrayAutoRelease sceneOrder = SaveSceneListOrder(); + OBSDataArrayAutoRelease transitions = SaveTransitions(); + OBSDataArrayAutoRelease quickTrData = SaveQuickTransitions(); + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + OBSDataAutoRelease saveData = GenerateSaveData(sceneOrder, quickTrData, ui->transitionDuration->value(), + transitions, scene, curProgramScene, savedProjectorList); + + obs_data_set_bool(saveData, "preview_locked", ui->preview->Locked()); + obs_data_set_bool(saveData, "scaling_enabled", ui->preview->IsFixedScaling()); + obs_data_set_int(saveData, "scaling_level", ui->preview->GetScalingLevel()); + obs_data_set_double(saveData, "scaling_off_x", ui->preview->GetScrollX()); + obs_data_set_double(saveData, "scaling_off_y", ui->preview->GetScrollY()); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_create(); + + obs_data_set_int(obj, "type2", (int)vcamConfig.type); + switch (vcamConfig.type) { + case VCamOutputType::Invalid: + case VCamOutputType::ProgramView: + case VCamOutputType::PreviewOutput: + break; + case VCamOutputType::SceneOutput: + obs_data_set_string(obj, "scene", vcamConfig.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + obs_data_set_string(obj, "source", vcamConfig.source.c_str()); + break; + } + + obs_data_set_obj(saveData, "virtual-camera", obj); + } + + if (api) { + if (!collectionModuleData) + collectionModuleData = obs_data_create(); + + api->on_save(collectionModuleData); + obs_data_set_obj(saveData, "modules", collectionModuleData); + } + + if (lastOutputResolution) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", lastOutputResolution->first); + obs_data_set_int(res, "y", lastOutputResolution->second); + obs_data_set_obj(saveData, "resolution", res); + } + + obs_data_set_int(saveData, "version", usingAbsoluteCoordinates ? 1 : 2); + + if (migrationBaseResolution && !usingAbsoluteCoordinates) { + OBSDataAutoRelease res = obs_data_create(); + obs_data_set_int(res, "x", migrationBaseResolution->first); + obs_data_set_int(res, "y", migrationBaseResolution->second); + obs_data_set_obj(saveData, "migration_resolution", res); + } + + if (!obs_data_save_json_pretty_safe(saveData, file, "tmp", "bak")) + blog(LOG_ERROR, "Could not save scene data to %s", file); +} + +void OBSBasic::DeferSaveBegin() +{ + os_atomic_inc_long(&disableSaving); +} + +void OBSBasic::DeferSaveEnd() +{ + long result = os_atomic_dec_long(&disableSaving); + if (result == 0) { + SaveProject(); + } +} + +static void LogFilter(obs_source_t *, obs_source_t *filter, void *v_val); + +static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent) +{ + OBSDataAutoRelease data = obs_data_get_obj(parent, name); + if (!data) + return; + + OBSSourceAutoRelease source = obs_load_source(data); + if (!source) + return; + + obs_set_output_source(channel, source); + + const char *source_name = obs_source_get_name(source); + blog(LOG_INFO, "[Loaded global audio device]: '%s'", source_name); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + obs_monitoring_type monitoring_type = obs_source_get_monitoring_type(source); + if (monitoring_type != OBS_MONITORING_TYPE_NONE) { + const char *type = (monitoring_type == OBS_MONITORING_TYPE_MONITOR_ONLY) ? "monitor only" + : "monitor and output"; + + blog(LOG_INFO, " - monitoring: %s", type); + } +} + +static inline bool HasAudioDevices(const char *source_id) +{ + const char *output_id = source_id; + obs_properties_t *props = obs_get_source_properties(output_id); + size_t count = 0; + + if (!props) + return false; + + obs_property_t *devices = obs_properties_get(props, "device_id"); + if (devices) + count = obs_property_list_item_count(devices); + + obs_properties_destroy(props); + + return count != 0; +} + +void OBSBasic::CreateFirstRunSources() +{ + bool hasDesktopAudio = HasAudioDevices(App()->OutputAudioSource()); + bool hasInputAudio = HasAudioDevices(App()->InputAudioSource()); + +#ifdef __APPLE__ + /* On macOS 13 and above, the SCK based audio capture provides a + * better alternative to the device-based audio capture. */ + if (__builtin_available(macOS 13.0, *)) { + hasDesktopAudio = false; + } +#endif + + if (hasDesktopAudio) + ResetAudioDevice(App()->OutputAudioSource(), "default", Str("Basic.DesktopDevice1"), 1); + if (hasInputAudio) + ResetAudioDevice(App()->InputAudioSource(), "default", Str("Basic.AuxDevice1"), 3); +} + +void OBSBasic::DisableRelativeCoordinates(bool enable) +{ + /* Allow disabling relative positioning to allow loading collections + * that cannot yet be migrated. */ + OBSDataAutoRelease priv = obs_get_private_data(); + obs_data_set_bool(priv, "AbsoluteCoordinates", enable); + usingAbsoluteCoordinates = enable; + + ui->actionRemigrateSceneCollection->setText(enable ? QTStr("Basic.MainMenu.SceneCollection.Migrate") + : QTStr("Basic.MainMenu.SceneCollection.Remigrate")); + ui->actionRemigrateSceneCollection->setEnabled(enable); +} + +void OBSBasic::CreateDefaultScene(bool firstStart) +{ + disableSaving++; + + ClearSceneData(); + InitDefaultTransitions(); + CreateDefaultQuickTransitions(); + ui->transitionDuration->setValue(300); + SetTransition(fadeTransition); + + DisableRelativeCoordinates(false); + OBSSceneAutoRelease scene = obs_scene_create(Str("Basic.Scene")); + + if (firstStart) + CreateFirstRunSources(); + + SetCurrentScene(scene, true); + + disableSaving--; +} + +static void ReorderItemByName(QListWidget *lw, const char *name, int newIndex) +{ + for (int i = 0; i < lw->count(); i++) { + QListWidgetItem *item = lw->item(i); + + if (strcmp(name, QT_TO_UTF8(item->text())) == 0) { + if (newIndex != i) { + item = lw->takeItem(i); + lw->insertItem(newIndex, item); + } + break; + } + } +} + +void OBSBasic::LoadSceneListOrder(obs_data_array_t *array) +{ + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + ReorderItemByName(ui->scenes, name, (int)i); + } +} + +void OBSBasic::LoadSavedProjectors(obs_data_array_t *array) +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + delete info; + } + savedProjectorsArray.clear(); + + size_t num = obs_data_array_count(array); + + for (size_t i = 0; i < num; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + + SavedProjectorInfo *info = new SavedProjectorInfo(); + info->monitor = obs_data_get_int(data, "monitor"); + info->type = static_cast(obs_data_get_int(data, "type")); + info->geometry = std::string(obs_data_get_string(data, "geometry")); + info->name = std::string(obs_data_get_string(data, "name")); + info->alwaysOnTop = obs_data_get_bool(data, "alwaysOnTop"); + info->alwaysOnTopOverridden = obs_data_get_bool(data, "alwaysOnTopOverridden"); + + savedProjectorsArray.emplace_back(info); + } +} + +static void 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; + string indent; + + for (int i = 0; i < val; i++) + indent += " "; + + blog(LOG_INFO, "%s- filter: '%s' (%s)", indent.c_str(), name, id); +} + +static bool 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; + string indent; + + for (int i = 0; i < indent_count; i++) + indent += " "; + + blog(LOG_INFO, "%s- source: '%s' (%s)", indent.c_str(), name, id); + + 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 OBSBasic::LogScenes() +{ + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Loaded scenes:"); + + for (int i = 0; i < ui->scenes->count(); i++) { + QListWidgetItem *item = ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + + obs_source_t *source = obs_scene_get_source(scene); + const char *name = obs_source_get_name(source); + + blog(LOG_INFO, "- scene '%s':", name); + obs_scene_enum_items(scene, LogSceneItem, (void *)(intptr_t)1); + obs_source_enum_filters(source, LogFilter, (void *)(intptr_t)1); + } + + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::Load(const char *file, bool remigrate) +{ + disableSaving++; + lastOutputResolution.reset(); + migrationBaseResolution.reset(); + + obs_data_t *data = obs_data_create_from_json_file_safe(file, "bak"); + if (!data) { + disableSaving--; + const auto path = filesystem::u8path(file); + const string name = path.stem().u8string(); + /* Check if file exists but failed to load. */ + if (filesystem::exists(path)) { + /* Assume the file is corrupt and rename it to allow + * for manual recovery if possible. */ + auto newPath = path; + newPath.concat(".invalid"); + + blog(LOG_WARNING, + "File exists but appears to be corrupt, renaming " + "to \"%s\" before continuing.", + newPath.filename().u8string().c_str()); + + error_code ec; + filesystem::rename(path, newPath, ec); + if (ec) { + blog(LOG_ERROR, "Failed renaming corrupt file with %d", ec.value()); + } + } + + blog(LOG_INFO, "No scene file found, creating default scene"); + + bool hasFirstRun = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + CreateDefaultScene(!hasFirstRun); + SaveProject(); + return; + } + + LoadData(data, file, remigrate); +} + +static inline void AddMissingFiles(void *data, obs_source_t *source) +{ + obs_missing_files_t *f = (obs_missing_files_t *)data; + obs_missing_files_t *sf = obs_source_get_missing_files(source); + + obs_missing_files_append(f, sf); + obs_missing_files_destroy(sf); +} + +static void ClearRelativePosCb(obs_data_t *data, void *) +{ + const string_view id = obs_data_get_string(data, "id"); + if (id != "scene" && id != "group") + return; + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + obs_data_array_enum( + items, + [](obs_data_t *data, void *) { + obs_data_unset_user_value(data, "pos_rel"); + obs_data_unset_user_value(data, "scale_rel"); + obs_data_unset_user_value(data, "scale_ref"); + obs_data_unset_user_value(data, "bounds_rel"); + }, + nullptr); +} + +void OBSBasic::LoadData(obs_data_t *data, const char *file, bool remigrate) +{ + ClearSceneData(); + ClearContextBar(); + + /* Exit OBS if clearing scene data failed for some reason. */ + if (clearingFailed) { + OBSMessageBox::critical(this, QTStr("SourceLeak.Title"), QTStr("SourceLeak.Text")); + close(); + return; + } + + InitDefaultTransitions(); + + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + OBSDataAutoRelease modulesObj = obs_data_get_obj(data, "modules"); + if (api) + api->on_preload(modulesObj); + + /* Keep a reference to "modules" data so plugins that are not loaded do + * not have their collection specific data lost. */ + collectionModuleData = obs_data_get_obj(data, "modules"); + + OBSDataArrayAutoRelease sceneOrder = obs_data_get_array(data, "scene_order"); + OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources"); + OBSDataArrayAutoRelease groups = obs_data_get_array(data, "groups"); + OBSDataArrayAutoRelease transitions = obs_data_get_array(data, "transitions"); + const char *sceneName = obs_data_get_string(data, "current_scene"); + const char *programSceneName = obs_data_get_string(data, "current_program_scene"); + const char *transitionName = obs_data_get_string(data, "current_transition"); + + if (!opt_starting_scene.empty()) { + programSceneName = opt_starting_scene.c_str(); + if (!IsPreviewProgramMode()) + sceneName = opt_starting_scene.c_str(); + } + + int newDuration = obs_data_get_int(data, "transition_duration"); + if (!newDuration) + newDuration = 300; + + if (!transitionName) + transitionName = obs_source_get_name(fadeTransition); + + const char *curSceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + obs_data_set_default_string(data, "name", curSceneCollection); + + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease curScene; + OBSSourceAutoRelease curProgramScene; + obs_source_t *curTransition; + + if (!name || !*name) + name = curSceneCollection; + + LoadAudioDevice(DESKTOP_AUDIO_1, 1, data); + LoadAudioDevice(DESKTOP_AUDIO_2, 2, data); + LoadAudioDevice(AUX_AUDIO_1, 3, data); + LoadAudioDevice(AUX_AUDIO_2, 4, data); + LoadAudioDevice(AUX_AUDIO_3, 5, data); + LoadAudioDevice(AUX_AUDIO_4, 6, data); + + if (!sources) { + sources = std::move(groups); + } else { + obs_data_array_push_back_array(sources, groups); + } + + /* Reset relative coordinate data if forcefully remigrating. */ + if (remigrate) { + obs_data_set_int(data, "version", 1); + obs_data_array_enum(sources, ClearRelativePosCb, nullptr); + } + + bool resetVideo = false; + bool disableRelativeCoords = false; + obs_video_info ovi; + + int64_t version = obs_data_get_int(data, "version"); + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (res) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + + /* Only migrate legacy collection if resolution is saved. */ + if (version < 2 && lastOutputResolution) { + obs_get_video_info(&ovi); + + uint32_t width = obs_data_get_int(res, "x"); + uint32_t height = obs_data_get_int(res, "y"); + + migrationBaseResolution = {width, height}; + + if (ovi.base_height != height || ovi.base_width != width) { + ovi.base_width = width; + ovi.base_height = height; + + /* Attempt to reset to last known canvas resolution for migration. */ + resetVideo = obs_reset_video(&ovi) == OBS_VIDEO_SUCCESS; + disableRelativeCoords = !resetVideo; + } + + /* If migration is possible, and it wasn't forced, back up the original file. */ + if (!disableRelativeCoords && !remigrate) { + auto path = filesystem::u8path(file); + auto backupPath = path.concat(".v1"); + if (!filesystem::exists(backupPath)) { + if (!obs_data_save_json_pretty_safe(data, backupPath.u8string().c_str(), "tmp", NULL)) { + blog(LOG_WARNING, + "Failed to create a backup of existing scene collection data!"); + } + } + } + } else if (version < 2) { + disableRelativeCoords = true; + } else if (OBSDataAutoRelease migration_res = obs_data_get_obj(data, "migration_resolution")) { + migrationBaseResolution = {obs_data_get_int(migration_res, "x"), obs_data_get_int(migration_res, "y")}; + } + + DisableRelativeCoordinates(disableRelativeCoords); + + obs_missing_files_t *files = obs_missing_files_create(); + obs_load_sources(sources, AddMissingFiles, files); + + if (resetVideo) + ResetVideo(); + if (transitions) + LoadTransitions(transitions, AddMissingFiles, files); + if (sceneOrder) + LoadSceneListOrder(sceneOrder); + + curTransition = FindTransition(transitionName); + if (!curTransition) + curTransition = fadeTransition; + + ui->transitionDuration->setValue(newDuration); + SetTransition(curTransition); + +retryScene: + curScene = obs_get_source_by_name(sceneName); + curProgramScene = obs_get_source_by_name(programSceneName); + + /* if the starting scene command line parameter is bad at all, + * fall back to original settings */ + if (!opt_starting_scene.empty() && (!curScene || !curProgramScene)) { + sceneName = obs_data_get_string(data, "current_scene"); + programSceneName = obs_data_get_string(data, "current_program_scene"); + opt_starting_scene.clear(); + goto retryScene; + } + + if (!curScene) { + auto find_scene_cb = [](void *source_ptr, obs_source_t *scene) { + *static_cast(source_ptr) = obs_source_get_ref(scene); + return false; + }; + obs_enum_scenes(find_scene_cb, &curScene); + } + + SetCurrentScene(curScene.Get(), true); + + if (!curProgramScene) + curProgramScene = std::move(curScene); + if (IsPreviewProgramMode()) + TransitionToScene(curProgramScene.Get(), true); + + /* ------------------- */ + + bool projectorSave = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SaveProjectors"); + + if (projectorSave) { + OBSDataArrayAutoRelease savedProjectors = obs_data_get_array(data, "saved_projectors"); + + if (savedProjectors) { + LoadSavedProjectors(savedProjectors); + OpenSavedProjectors(); + activateWindow(); + } + } + + /* ------------------- */ + + std::string file_base = strrchr(file, '/') + 1; + file_base.erase(file_base.size() - 5, 5); + + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollection", name); + config_set_string(App()->GetUserConfig(), "Basic", "SceneCollectionFile", file_base.c_str()); + + OBSDataArrayAutoRelease quickTransitionData = obs_data_get_array(data, "quick_transitions"); + LoadQuickTransitions(quickTransitionData); + + RefreshQuickTransitions(); + + bool previewLocked = obs_data_get_bool(data, "preview_locked"); + ui->preview->SetLocked(previewLocked); + ui->actionLockPreview->setChecked(previewLocked); + + /* ---------------------- */ + + bool fixedScaling = obs_data_get_bool(data, "scaling_enabled"); + int scalingLevel = (int)obs_data_get_int(data, "scaling_level"); + float scrollOffX = (float)obs_data_get_double(data, "scaling_off_x"); + float scrollOffY = (float)obs_data_get_double(data, "scaling_off_y"); + + if (fixedScaling) { + ui->preview->SetScalingLevel(scalingLevel); + ui->preview->SetScrollingOffset(scrollOffX, scrollOffY); + } + ui->preview->SetFixedScaling(fixedScaling); + + emit ui->preview->DisplayResized(); + + if (vcamEnabled) { + OBSDataAutoRelease obj = obs_data_get_obj(data, "virtual-camera"); + + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type2"); + if (vcamConfig.type == VCamOutputType::Invalid) + vcamConfig.type = (VCamOutputType)obs_data_get_int(obj, "type"); + + if (vcamConfig.type == VCamOutputType::Invalid) { + VCamInternalType internal = (VCamInternalType)obs_data_get_int(obj, "internal"); + + switch (internal) { + case VCamInternalType::Default: + vcamConfig.type = VCamOutputType::ProgramView; + break; + case VCamInternalType::Preview: + vcamConfig.type = VCamOutputType::PreviewOutput; + break; + } + } + vcamConfig.scene = obs_data_get_string(obj, "scene"); + vcamConfig.source = obs_data_get_string(obj, "source"); + } + + if (obs_data_has_user_value(data, "resolution")) { + OBSDataAutoRelease res = obs_data_get_obj(data, "resolution"); + if (obs_data_has_user_value(res, "x") && obs_data_has_user_value(res, "y")) { + lastOutputResolution = {obs_data_get_int(res, "x"), obs_data_get_int(res, "y")}; + } + } + + /* ---------------------- */ + + if (api) + api->on_load(modulesObj); + + obs_data_release(data); + + if (!opt_starting_scene.empty()) + opt_starting_scene.clear(); + + if (opt_start_streaming && !safe_mode) { + blog(LOG_INFO, "Starting stream due to command line parameter"); + QMetaObject::invokeMethod(this, "StartStreaming", Qt::QueuedConnection); + opt_start_streaming = false; + } + + if (opt_start_recording && !safe_mode) { + blog(LOG_INFO, "Starting recording due to command line parameter"); + QMetaObject::invokeMethod(this, "StartRecording", Qt::QueuedConnection); + opt_start_recording = false; + } + + if (opt_start_replaybuffer && !safe_mode) { + QMetaObject::invokeMethod(this, "StartReplayBuffer", Qt::QueuedConnection); + opt_start_replaybuffer = false; + } + + if (opt_start_virtualcam && !safe_mode) { + QMetaObject::invokeMethod(this, "StartVirtualCam", Qt::QueuedConnection); + opt_start_virtualcam = false; + } + + LogScenes(); + + if (!App()->IsMissingFilesCheckDisabled()) + ShowMissingFilesDialog(files); + + disableSaving--; + + if (vcamEnabled) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); +} + +constexpr std::string_view OBSServiceFileName = "service.json"; + +void OBSBasic::SaveService() +{ + if (!service) + return; + + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), "tmp", "bak")) { + blog(LOG_WARNING, "Failed to save service"); + } +} + +bool OBSBasic::LoadService() +{ + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + + const char *type; + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on WHIP if needed */ + if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + const char *option = config_get_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); + + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); + + const char *encoder_codec = obs_get_encoder_codec(option); + if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) + config_set_string(activeConfiguration, "AdvOut", "AudioEncoder", "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; + +extern void CheckExistingCookieId(); + +#ifdef __APPLE__ +#define DEFAULT_CONTAINER "fragmented_mov" +#elif OBS_RELEASE_CANDIDATE == 0 && OBS_BETA == 0 +#define DEFAULT_CONTAINER "mkv" +#else +#define DEFAULT_CONTAINER "hybrid_mp4" +#endif + +bool OBSBasic::InitBasicConfigDefaults() +{ + QList screens = QGuiApplication::screens(); + + if (!screens.size()) { + OBSErrorBox(NULL, "There appears to be no monitors. Er, this " + "technically shouldn't be possible."); + return false; + } + + QScreen *primaryScreen = QGuiApplication::primaryScreen(); + + uint32_t cx = primaryScreen->size().width(); + uint32_t cy = primaryScreen->size().height(); + + cx *= devicePixelRatioF(); + cy *= devicePixelRatioF(); + + bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); + + /* use 1920x1080 for new default base res if main monitor is above + * 1920x1080, but don't apply for people from older builds -- only to + * new users */ + if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { + cx = 1920; + cy = 1080; + } + + bool changed = false; + + /* ----------------------------------------------------- */ + /* move over old FFmpeg track settings */ + if (config_has_user_value(activeConfiguration, "AdvOut", "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", "Pre22.1Settings")) { + + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); + config_set_bool(activeConfiguration, "AdvOut", "Pre22.1Settings", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move over mixer values in advanced if older config */ + if (config_has_user_value(activeConfiguration, "AdvOut", "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecTracks")) { + + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", "RecTrackIndex"); + track = 1ULL << (track - 1); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", track); + config_remove_value(activeConfiguration, "AdvOut", "RecTrackIndex"); + changed = true; + } + + /* ----------------------------------------------------- */ + /* set twitch chat extensions to "both" if prev version */ + /* is under 24.1 */ + if (config_get_bool(App()->GetUserConfig(), "General", "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); + changed = true; + } + + /* ----------------------------------------------------- */ + /* move bitrate enforcement setting to new value */ + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && + !config_has_user_value(activeConfiguration, "Stream1", "MovedOldEnforce")) { + bool enforce = config_get_bool(activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", "MovedOldEnforce", true); + changed = true; + } + + /* ----------------------------------------------------- */ + /* enforce minimum retry delay of 1 second prior to 27.1 */ + if (config_has_user_value(activeConfiguration, "Output", "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", "RetryDelay"); + if (retryDelay < 1) { + config_set_uint(activeConfiguration, "Output", "RetryDelay", 1); + changed = true; + } + } + + /* ----------------------------------------------------- */ + /* Migrate old container selection (if any) to new key. */ + + auto MigrateFormat = [&](const char *section) { + bool has_old_key = config_has_user_value(activeConfiguration, section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, section, "RecFormat2"); + if (!has_new_key && !has_old_key) + return; + + string old_format = + config_get_string(activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); + string new_format = old_format; + if (old_format == "ts") + new_format = "mpegts"; + else if (old_format == "m3u8") + new_format = "hls"; + else if (old_format == "fmp4") + new_format = "fragmented_mp4"; + else if (old_format == "fmov") + new_format = "fragmented_mov"; + + if (new_format != old_format || !has_new_key) { + config_set_string(activeConfiguration, section, "RecFormat2", new_format.c_str()); + changed = true; + } + }; + + MigrateFormat("AdvOut"); + MigrateFormat("SimpleOutput"); + + /* ----------------------------------------------------- */ + /* Migrate output scale setting to GPU scaling options. */ + + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); + } + + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RecRescaleFilter", OBS_SCALE_BILINEAR); + } + + /* ----------------------------------------------------- */ + + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } + + /* ----------------------------------------------------- */ + + config_set_default_string(activeConfiguration, "Output", "Mode", "Simple"); + + config_set_default_bool(activeConfiguration, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); + + config_set_default_string(activeConfiguration, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); + config_set_default_string(activeConfiguration, "SimpleOutput", "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", 512); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); + config_set_default_uint(activeConfiguration, "SimpleOutput", "RecTracks", (1 << 0)); + + config_set_default_bool(activeConfiguration, "AdvOut", "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", "obs_x264"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecType", "Standard"); + + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", 1); + + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", 160); + + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", "RecSplitFileSize", 2048); + + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); + + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); + + /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_string(activeConfiguration, "Output", "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); + + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", true); + + config_set_default_bool(activeConfiguration, "Output", "Reconnect", true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", 25); + + config_set_default_string(activeConfiguration, "Output", "BindIP", "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); + config_set_default_bool(activeConfiguration, "Output", "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", "LowLatencyEnable", false); + + int i = 0; + uint32_t scale_cx = cx; + uint32_t scale_cy = cy; + + /* use a default scaled resolution that has a pixel count no higher + * than 1280x720 */ + while (((scale_cx * scale_cy) > (1280 * 720)) && scaled_vals[i] > 0.0) { + double scale = scaled_vals[i++]; + scale_cx = uint32_t(double(cx) / scale); + scale_cy = uint32_t(double(cy) / scale); + } + + config_set_default_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + + /* don't allow OutputCX/OutputCY to be susceptible to defaults + * changing */ + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); + } + + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(activeConfiguration, "Video", "HdrNominalPeakLevel", 1000); + + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceId", "default"); + config_set_default_string(activeConfiguration, "Audio", "MonitoringDeviceName", + Str("Basic.Settings.Advanced.Audio.MonitoringDevice" + ".Default")); + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); + config_set_default_double(activeConfiguration, "Audio", "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", 0); + + CheckExistingCookieId(); + + return true; +} + +extern bool EncoderAvailable(const char *encoder); + +void OBSBasic::InitBasicConfigDefaults2() +{ + bool oldEncDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; + + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string(activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + + const char *aac_default = "ffmpeg_aac"; + if (EncoderAvailable("CoreAudio_AAC")) + aac_default = "CoreAudio_AAC"; + else if (EncoderAvailable("libfdk_aac")) + aac_default = "libfdk_aac"; + + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", "RecAudioEncoder", aac_default); +} + +bool OBSBasic::InitBasicConfig() +{ + ProfileScope("OBSBasic::InitBasicConfig"); + + RefreshProfiles(true); + + const std::string currentProfileName{config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::optional currentProfile = GetProfileByName(currentProfileName); + const std::optional foundProfile = GetProfileByName(opt_starting_profile); + + try { + if (foundProfile) { + ActivateProfile(foundProfile.value()); + } else if (currentProfile) { + ActivateProfile(currentProfile.value()); + } else { + const OBSProfile &newProfile = CreateProfile(currentProfileName); + ActivateProfile(newProfile); + } + } catch (const std::logic_error &) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", -1); + return false; + } + + return true; +} + +void OBSBasic::InitOBSCallbacks() +{ + ProfileScope("OBSBasic::InitOBSCallbacks"); + + signalHandlers.reserve(signalHandlers.size() + 9); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_create", OBSBasic::SourceCreated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove", OBSBasic::SourceRemoved, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate", OBSBasic::SourceActivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate", OBSBasic::SourceDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_activate", OBSBasic::SourceAudioActivated, + this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_audio_deactivate", + OBSBasic::SourceAudioDeactivated, this); + signalHandlers.emplace_back(obs_get_signal_handler(), "source_rename", OBSBasic::SourceRenamed, this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_add", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); + signalHandlers.emplace_back( + obs_get_signal_handler(), "source_filter_remove", + [](void *data, calldata_t *) { + QMetaObject::invokeMethod(static_cast(data), "UpdateEditMenu", + Qt::QueuedConnection); + }, + this); +} + +void OBSBasic::InitPrimitives() +{ + ProfileScope("OBSBasic::InitPrimitives"); + + 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(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(0.0f, 1.0f); + boxLeft = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 0.0f); + gs_vertex2f(1.0f, 0.0f); + boxTop = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(1.0f, 0.0f); + gs_vertex2f(1.0f, 1.0f); + boxRight = gs_render_save(); + + gs_render_start(true); + gs_vertex2f(0.0f, 1.0f); + gs_vertex2f(1.0f, 1.0f); + boxBottom = gs_render_save(); + + gs_render_start(true); + for (int i = 0; i <= 360; i += (360 / 20)) { + float pos = RAD(float(i)); + gs_vertex2f(cosf(pos), sinf(pos)); + } + circle = gs_render_save(); + + InitSafeAreas(&actionSafeMargin, &graphicsSafeMargin, &fourByThreeSafeMargin, &leftLine, &topLine, &rightLine); + obs_leave_graphics(); +} + +void OBSBasic::ReplayBufferActionTriggered() +{ + if (outputHandler->ReplayBufferActive()) + StopReplayBuffer(); + else + StartReplayBuffer(); +}; + +void OBSBasic::ResetOutputs() +{ + ProfileScope("OBSBasic::ResetOutputs"); + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool advOut = astrcmpi(mode, "Advanced") == 0; + + if ((!outputHandler || !outputHandler->Active()) && + (!setupStreamingGuard.valid() || + setupStreamingGuard.wait_for(std::chrono::seconds{0}) == std::future_status::ready)) { + outputHandler.reset(); + outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); + + emit ReplayBufEnabled(outputHandler->replayBuffer); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setEnabled(!!outputHandler->replayBuffer); + + UpdateIsRecordingPausable(); + } else { + outputHandler->Update(); + } +} + +#define STARTUP_SEPARATOR "==== Startup complete ===============================================" +#define SHUTDOWN_SEPARATOR "==== Shutting down ==================================================" + +#define UNSUPPORTED_ERROR \ + "Failed to initialize video:\n\nRequired graphics API functionality " \ + "not found. Your GPU may not be supported." + +#define UNKNOWN_ERROR \ + "Failed to initialize video. Your GPU may not be supported, " \ + "or your graphics drivers may need to be updated." + +static inline void LogEncoders() +{ + constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL; + + auto list_encoders = [](obs_encoder_type type) { + size_t idx = 0; + const char *encoder_type; + + while (obs_enum_encoder_types(idx++, &encoder_type)) { + if (obs_get_encoder_caps(encoder_type) & hide_flags || + obs_get_encoder_type(encoder_type) != type) { + continue; + } + + blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type)); + } + }; + + blog(LOG_INFO, "---------------------------------"); + blog(LOG_INFO, "Available Encoders:"); + blog(LOG_INFO, " Video Encoders:"); + list_encoders(OBS_ENCODER_VIDEO); + blog(LOG_INFO, " Audio Encoders:"); + list_encoders(OBS_ENCODER_AUDIO); +} + +void OBSBasic::OBSInit() +{ + ProfileScope("OBSBasic::OBSInit"); + + if (!InitBasicConfig()) + throw "Failed to load basic.ini"; + if (!ResetAudio()) + throw "Failed to initialize audio"; + + int ret = 0; + + ret = ResetVideo(); + + switch (ret) { + case OBS_VIDEO_MODULE_NOT_FOUND: + throw "Failed to initialize video: Graphics module not found"; + case OBS_VIDEO_NOT_SUPPORTED: + throw UNSUPPORTED_ERROR; + case OBS_VIDEO_INVALID_PARAM: + throw "Failed to initialize video: Invalid parameters"; + default: + if (ret != OBS_VIDEO_SUCCESS) + throw UNKNOWN_ERROR; + } + + /* load audio monitoring */ + if (obs_audio_monitoring_available()) { + const char *device_name = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string(activeConfiguration, "Audio", "MonitoringDeviceId"); + + obs_set_audio_monitoring_device(device_name, device_id); + + blog(LOG_INFO, "Audio monitoring device:\n\tname: %s\n\tid: %s", device_name, device_id); + } + + InitOBSCallbacks(); + InitHotkeys(); + ui->preview->Init(); + + /* hack to prevent elgato from loading its own QtNetwork that it tries + * to ship with */ +#if defined(_WIN32) && !defined(_DEBUG) + LoadLibraryW(L"Qt6Network"); +#endif + struct obs_module_failure_info mfi; + + /* Safe Mode disables third-party plugins so we don't need to add earch + * paths outside the OBS bundle/installation. */ + if (safe_mode || disable_3p_plugins) { + SetSafeModuleNames(); + } else { + AddExtraModulePaths(); + } + + /* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code. + + Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information. + */ + RefreshSceneCollections(true); + + blog(LOG_INFO, "---------------------------------"); + obs_load_all_modules2(&mfi); + blog(LOG_INFO, "---------------------------------"); + obs_log_loaded_modules(); + blog(LOG_INFO, "---------------------------------"); + obs_post_load_modules(); + + BPtr failed_modules = mfi.failed_modules; + +#ifdef BROWSER_AVAILABLE + cef = obs_browser_init_panel(); + cef_js_avail = cef && obs_browser_qcef_version() >= 3; +#endif + + vcamEnabled = (obs_get_output_flags(VIRTUAL_CAM_ID) & OBS_OUTPUT_VIDEO) != 0; + if (vcamEnabled) { + emit VirtualCamEnabled(); + } + + UpdateProfileEncoders(); + + LogEncoders(); + + blog(LOG_INFO, STARTUP_SEPARATOR); + + if (!InitService()) + throw "Failed to initialize service"; + + ResetOutputs(); + CreateHotkeys(); + + InitPrimitives(); + + sceneDuplicationMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode"); + swapScenesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode"); + editPropertiesMode = config_get_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode"); + + if (!opt_studio_mode) { + SetPreviewProgramMode(config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode")); + } else { + SetPreviewProgramMode(true); + opt_studio_mode = false; + } + +#define SET_VISIBILITY(name, control) \ + do { \ + if (config_has_user_value(App()->GetUserConfig(), "BasicWindow", name)) { \ + bool visible = config_get_bool(App()->GetUserConfig(), "BasicWindow", name); \ + ui->control->setChecked(visible); \ + } \ + } while (false) + + SET_VISIBILITY("ShowListboxToolbars", toggleListboxToolbars); + SET_VISIBILITY("ShowStatusBar", toggleStatusBar); +#undef SET_VISIBILITY + + bool sourceIconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + ui->toggleSourceIcons->setChecked(sourceIconsVisible); + + bool contextVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars"); + ui->toggleContextBar->setChecked(contextVisible); + ui->contextContainer->setVisible(contextVisible); + if (contextVisible) + UpdateContextBar(true); + UpdateEditMenu(); + + { + ProfileScope("OBSBasic::Load"); + const std::string sceneCollectionName{ + config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection")}; + const std::optional configuredCollection = + GetSceneCollectionByName(sceneCollectionName); + const std::optional foundCollection = + GetSceneCollectionByName(opt_starting_collection); + + if (foundCollection) { + ActivateSceneCollection(foundCollection.value()); + } else if (configuredCollection) { + ActivateSceneCollection(configuredCollection.value()); + } else { + disableSaving--; + SetupNewSceneCollection(sceneCollectionName); + disableSaving++; + } + + disableSaving--; + if (foundCollection || configuredCollection) { + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED); + } + OnEvent(OBS_FRONTEND_EVENT_SCENE_CHANGED); + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + disableSaving++; + } + + loaded = true; + + previewEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled"); + + if (!previewEnabled && !IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, + Q_ARG(bool, previewEnabled)); + else if (!previewEnabled && IsPreviewProgramMode()) + QMetaObject::invokeMethod(this, "EnablePreviewDisplay", Qt::QueuedConnection, Q_ARG(bool, true)); + + disableSaving--; + + auto addDisplay = [this](OBSQTDisplay *window) { + obs_display_add_draw_callback(window->GetDisplay(), OBSBasic::RenderMain, this); + + struct obs_video_info ovi; + if (obs_get_video_info(&ovi)) + ResizePreview(ovi.base_width, ovi.base_height); + }; + + connect(ui->preview, &OBSQTDisplay::DisplayCreated, addDisplay); + + /* Show the main window, unless the tray icon isn't available + * or neither the setting nor flag for starting minimized is set. */ + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool hideWindowOnStart = QSystemTrayIcon::isSystemTrayAvailable() && sysTrayEnabled && + (opt_minimize_tray || sysTrayWhenStarted); + +#ifdef _WIN32 + SetWin32DropStyle(this); + + if (!hideWindowOnStart) + show(); +#endif + + bool alwaysOnTop = config_get_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop"); + +#ifdef ENABLE_WAYLAND + bool isWayland = obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND; +#else + bool isWayland = false; +#endif + + if (!isWayland && (alwaysOnTop || opt_always_on_top)) { + SetAlwaysOnTop(this, true); + ui->actionAlwaysOnTop->setChecked(true); + } else if (isWayland) { + if (opt_always_on_top) + blog(LOG_INFO, "Always On Top not available on Wayland, ignoring."); + ui->actionAlwaysOnTop->setEnabled(false); + ui->actionAlwaysOnTop->setVisible(false); + } + +#ifndef _WIN32 + if (!hideWindowOnStart) + show(); +#endif + + /* setup stats dock */ + OBSBasicStats *statsDlg = new OBSBasicStats(statsDock, false); + statsDock->setWidget(statsDlg); + + /* ----------------------------- */ + /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif + +#ifdef BROWSER_AVAILABLE + if (cef) { + QAction *action = new QAction(QTStr("Basic.MainMenu.Docks." + "CustomBrowserDocks"), + this); + ui->menuDocks->insertAction(ui->scenesDock->toggleViewAction(), action); + connect(action, &QAction::triggered, this, &OBSBasic::ManageExtraBrowserDocks); + ui->menuDocks->insertSeparator(ui->scenesDock->toggleViewAction()); + + LoadExtraBrowserDocks(); + } +#endif + +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + NewYouTubeAppDock(); +#endif + + const char *dockStateStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GetUserConfig(), "General", "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool(App()->GetUserConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GetUserConfig(), "General", "ResetDockLock23", true); + config_remove_value(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); + + SystemTray(true); + + TaskbarOverlayInit(); + +#ifdef __APPLE__ + disableColorSpaceConversion(this); +#endif + + bool has_last_version = config_has_user_value(App()->GetAppConfig(), "General", "LastVersion"); + bool first_run = config_get_bool(App()->GetUserConfig(), "General", "FirstRun"); + + if (!first_run) { + config_set_bool(App()->GetUserConfig(), "General", "FirstRun", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + + if (!first_run && !has_last_version && !Active()) + QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); + +#if (defined(_WIN32) || defined(__APPLE__)) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) + /* Automatically set branch to "beta" the first time a pre-release build is run. */ + if (!config_get_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn")) { + config_set_string(App()->GetAppConfig(), "General", "UpdateBranch", "beta"); + config_set_bool(App()->GetAppConfig(), "General", "AutoBetaOptIn", true); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + } +#endif + TimedCheckForUpdates(); + + ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + if (config_get_bool(activeConfiguration, "General", "OpenStatsOnStartup")) + on_stats_triggered(); + + OBSBasicStats::InitializeValues(); + + /* ----------------------- */ + /* Add multiview menu */ + + ui->viewMenu->addSeparator(); + + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); + connect(ui->viewMenu->menuAction(), &QAction::hovered, this, &OBSBasic::UpdateMultiviewProjectorMenu); + + ui->sources->UpdateIcons(); + +#if !defined(_WIN32) + delete ui->actionShowCrashLogs; + delete ui->actionUploadLastCrashLog; + delete ui->menuCrashLogs; + delete ui->actionRepair; + ui->actionShowCrashLogs = nullptr; + ui->actionUploadLastCrashLog = nullptr; + ui->menuCrashLogs = nullptr; + ui->actionRepair = nullptr; +#if !defined(__APPLE__) + delete ui->actionCheckForUpdates; + ui->actionCheckForUpdates = nullptr; +#endif +#endif + +#ifdef __APPLE__ + /* Remove OBS' Fullscreen Interface menu in favor of the one macOS adds by default */ + delete ui->actionFullscreenInterface; + ui->actionFullscreenInterface = nullptr; +#else + /* Don't show menu to raise macOS-only permissions dialog */ + delete ui->actionShowMacPermissions; + ui->actionShowMacPermissions = nullptr; +#endif + +#if defined(_WIN32) || defined(__APPLE__) + if (App()->IsUpdaterDisabled()) { + ui->actionCheckForUpdates->setEnabled(false); +#if defined(_WIN32) + ui->actionRepair->setEnabled(false); +#endif + } +#endif + +#ifndef WHATSNEW_ENABLED + delete ui->actionShowWhatsNew; + ui->actionShowWhatsNew = nullptr; +#endif + + if (safe_mode) { + ui->actionRestartSafe->setText(QTStr("Basic.MainMenu.Help.RestartNormal")); + } + + UpdatePreviewProgramIndicators(); + OnFirstLoad(); + + if (!hideWindowOnStart) + activateWindow(); + + /* ------------------------------------------- */ + /* display warning message for failed modules */ + + if (mfi.count) { + QString failed_plugins; + + char **plugin = mfi.failed_modules; + while (*plugin) { + failed_plugins += *plugin; + failed_plugins += "\n"; + plugin++; + } + + QString failed_msg = QTStr("PluginsFailedToLoad.Text").arg(failed_plugins); + OBSMessageBox::warning(this, QTStr("PluginsFailedToLoad.Title"), failed_msg); + } +} + +void OBSBasic::OnFirstLoad() +{ + OnEvent(OBS_FRONTEND_EVENT_FINISHED_LOADING); + +#ifdef WHATSNEW_ENABLED + /* Attempt to load init screen if available */ + if (cef) { + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); + } +#endif + + Auth::Load(); + + bool showLogViewerOnStartup = config_get_bool(App()->GetUserConfig(), "LogViewer", "ShowLogStartup"); + + if (showLogViewerOnStartup) + on_actionViewCurrentLog_triggered(); +} + +/* shows a "what's new" page on startup of new versions using CEF */ +void OBSBasic::ReceivedIntroJson(const QString &text) +{ +#ifdef WHATSNEW_ENABLED + if (closing) + return; + + WhatsNewList items; + try { + nlohmann::json json = nlohmann::json::parse(text.toStdString()); + items = json.get(); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "Parsing whatsnew data failed: %s", e.what()); + return; + } + + std::string info_url; + int info_increment = -1; + + /* check to see if there's an info page for this version */ + for (const WhatsNewItem &item : items) { + if (item.os) { + WhatsNewPlatforms platforms = *item.os; +#ifdef _WIN32 + if (!platforms.windows) + continue; +#elif defined(__APPLE__) + if (!platforms.macos) + continue; +#else + if (!platforms.linux) + continue; +#endif + } + + int major = 0; + int minor = 0; + + sscanf(item.version.c_str(), "%d.%d", &major, &minor); + if (major == LIBOBS_API_MAJOR_VER && minor == LIBOBS_API_MINOR_VER && + item.RC == OBS_RELEASE_CANDIDATE && item.Beta == OBS_BETA) { + info_url = item.url; + info_increment = item.increment; + } + } + + /* this version was not found, or no info for this version */ + if (info_increment == -1) { + return; + } + +#if OBS_RELEASE_CANDIDATE > 0 + constexpr const char *lastInfoVersion = "InfoLastRCVersion"; +#elif OBS_BETA > 0 + constexpr const char *lastInfoVersion = "InfoLastBetaVersion"; +#else + constexpr const char *lastInfoVersion = "InfoLastVersion"; +#endif + constexpr uint64_t currentVersion = (uint64_t)LIBOBS_API_VER << 16ULL | OBS_RELEASE_CANDIDATE << 8ULL | + OBS_BETA; + uint64_t lastVersion = config_get_uint(App()->GetAppConfig(), "General", lastInfoVersion); + int current_version_increment = -1; + + if ((lastVersion & ~0xFFFF0000ULL) < (currentVersion & ~0xFFFF0000ULL)) { + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + config_set_uint(App()->GetAppConfig(), "General", lastInfoVersion, currentVersion); + } else { + current_version_increment = config_get_int(App()->GetAppConfig(), "General", "InfoIncrement"); + } + + if (info_increment <= current_version_increment) { + return; + } + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", info_increment); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + cef->init_browser(); + + WhatsNewBrowserInitThread *wnbit = new WhatsNewBrowserInitThread(QT_UTF8(info_url.c_str())); + + connect(wnbit, &WhatsNewBrowserInitThread::Result, this, &OBSBasic::ShowWhatsNew, Qt::QueuedConnection); + + whatsNewInitThread.reset(wnbit); + whatsNewInitThread->start(); + +#else + UNUSED_PARAMETER(text); +#endif +} + +void OBSBasic::ShowWhatsNew(const QString &url) +{ +#ifdef BROWSER_AVAILABLE + if (closing) + return; + + if (obsWhatsNew) { + obsWhatsNew->close(); + } + + obsWhatsNew = new OBSWhatsNew(this, QT_TO_UTF8(url)); +#else + UNUSED_PARAMETER(url); +#endif +} + +void OBSBasic::UpdateMultiviewProjectorMenu() +{ + ui->multiviewProjectorMenu->clear(); + AddProjectorMenuMonitors(ui->multiviewProjectorMenu, this, &OBSBasic::OpenMultiviewProjector); +} + +void OBSBasic::InitHotkeys() +{ + ProfileScope("OBSBasic::InitHotkeys"); + + struct obs_hotkeys_translations t = {}; + t.insert = Str("Hotkeys.Insert"); + t.del = Str("Hotkeys.Delete"); + t.home = Str("Hotkeys.Home"); + t.end = Str("Hotkeys.End"); + t.page_up = Str("Hotkeys.PageUp"); + t.page_down = Str("Hotkeys.PageDown"); + t.num_lock = Str("Hotkeys.NumLock"); + t.scroll_lock = Str("Hotkeys.ScrollLock"); + t.caps_lock = Str("Hotkeys.CapsLock"); + t.backspace = Str("Hotkeys.Backspace"); + t.tab = Str("Hotkeys.Tab"); + t.print = Str("Hotkeys.Print"); + t.pause = Str("Hotkeys.Pause"); + t.left = Str("Hotkeys.Left"); + t.right = Str("Hotkeys.Right"); + t.up = Str("Hotkeys.Up"); + t.down = Str("Hotkeys.Down"); +#ifdef _WIN32 + t.meta = Str("Hotkeys.Windows"); +#else + t.meta = Str("Hotkeys.Super"); +#endif + t.menu = Str("Hotkeys.Menu"); + t.space = Str("Hotkeys.Space"); + t.numpad_num = Str("Hotkeys.NumpadNum"); + t.numpad_multiply = Str("Hotkeys.NumpadMultiply"); + t.numpad_divide = Str("Hotkeys.NumpadDivide"); + t.numpad_plus = Str("Hotkeys.NumpadAdd"); + t.numpad_minus = Str("Hotkeys.NumpadSubtract"); + t.numpad_decimal = Str("Hotkeys.NumpadDecimal"); + t.apple_keypad_num = Str("Hotkeys.AppleKeypadNum"); + t.apple_keypad_multiply = Str("Hotkeys.AppleKeypadMultiply"); + t.apple_keypad_divide = Str("Hotkeys.AppleKeypadDivide"); + t.apple_keypad_plus = Str("Hotkeys.AppleKeypadAdd"); + t.apple_keypad_minus = Str("Hotkeys.AppleKeypadSubtract"); + t.apple_keypad_decimal = Str("Hotkeys.AppleKeypadDecimal"); + t.apple_keypad_equal = Str("Hotkeys.AppleKeypadEqual"); + t.mouse_num = Str("Hotkeys.MouseButton"); + t.escape = Str("Hotkeys.Escape"); + obs_hotkeys_set_translations(&t); + + obs_hotkeys_set_audio_hotkeys_translations(Str("Mute"), Str("Unmute"), Str("Push-to-mute"), + Str("Push-to-talk")); + + obs_hotkeys_set_sceneitem_hotkeys_translations(Str("SceneItemShow"), Str("SceneItemHide")); + + obs_hotkey_enable_callback_rerouting(true); + obs_hotkey_set_callback_routing_func(OBSBasic::HotkeyTriggered, this); +} + +void OBSBasic::ProcessHotkey(obs_hotkey_id id, bool pressed) +{ + obs_hotkey_trigger_routed_callback(id, pressed); +} + +void OBSBasic::HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed) +{ + OBSBasic &basic = *static_cast(data); + QMetaObject::invokeMethod(&basic, "ProcessHotkey", Q_ARG(obs_hotkey_id, id), Q_ARG(bool, pressed)); +} + +void OBSBasic::CreateHotkeys() +{ + ProfileScope("OBSBasic::CreateHotkeys"); + + auto LoadHotkeyData = [&](const char *name) -> OBSData { + const char *info = config_get_string(activeConfiguration, "Hotkeys", name); + if (!info) + return {}; + + OBSDataAutoRelease data = obs_data_create_from_json(info); + if (!data) + return {}; + + return data.Get(); + }; + + auto LoadHotkey = [&](obs_hotkey_id id, const char *name) { + OBSDataArrayAutoRelease array = obs_data_get_array(LoadHotkeyData(name), "bindings"); + + obs_hotkey_load(id, array); + }; + + auto LoadHotkeyPair = [&](obs_hotkey_pair_id id, const char *name0, const char *name1, + const char *oldName = NULL) { + if (oldName) { + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); + if (info) { + config_set_string(activeConfiguration, "Hotkeys", name0, info); + config_set_string(activeConfiguration, "Hotkeys", name1, info); + config_remove_value(activeConfiguration, "Hotkeys", oldName); + activeConfiguration.Save(); + } + } + OBSDataArrayAutoRelease array0 = obs_data_get_array(LoadHotkeyData(name0), "bindings"); + OBSDataArrayAutoRelease array1 = obs_data_get_array(LoadHotkeyData(name1), "bindings"); + + obs_hotkey_pair_load(id, array0, array1); + }; + +#define MAKE_CALLBACK(pred, method, log_action) \ + [](void *data, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) { \ + OBSBasic &basic = *static_cast(data); \ + if ((pred) && pressed) { \ + blog(LOG_INFO, log_action " due to hotkey"); \ + method(); \ + return true; \ + } \ + return false; \ + } + + streamingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartStreaming", Str("Basic.Main.StartStreaming"), "OBSBasic.StopStreaming", + Str("Basic.Main.StopStreaming"), + MAKE_CALLBACK(!basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StartStreaming, + "Starting stream"), + MAKE_CALLBACK(basic.outputHandler->StreamingActive() && !basic.streamingStarting, basic.StopStreaming, + "Stopping stream"), + this, this); + LoadHotkeyPair(streamingHotkeys, "OBSBasic.StartStreaming", "OBSBasic.StopStreaming"); + + auto cb = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic &basic = *static_cast(data); + if (basic.outputHandler->StreamingActive() && pressed) { + basic.ForceStopStreaming(); + } + }; + + forceStreamingStopHotkey = obs_hotkey_register_frontend("OBSBasic.ForceStopStreaming", + Str("Basic.Main.ForceStopStreaming"), cb, this); + LoadHotkey(forceStreamingStopHotkey, "OBSBasic.ForceStopStreaming"); + + recordingHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartRecording", Str("Basic.Main.StartRecording"), "OBSBasic.StopRecording", + Str("Basic.Main.StopRecording"), + MAKE_CALLBACK(!basic.outputHandler->RecordingActive() && !basic.recordingStarted, basic.StartRecording, + "Starting recording"), + MAKE_CALLBACK(basic.outputHandler->RecordingActive() && basic.recordingStarted, basic.StopRecording, + "Stopping recording"), + this, this); + LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + + pauseHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.isRecordingPausable && !basic.recordingPaused, + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.isRecordingPausable && basic.recordingPaused, + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", "OBSBasic.UnpauseRecording"); + + splitFileHotkey = obs_hotkey_register_frontend( + "OBSBasic.SplitFile", Str("Basic.Main.SplitFile"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_split_file(); + }, + this); + LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile"); + + addChapterHotkey = obs_hotkey_register_frontend( + "OBSBasic.AddChapterMarker", Str("Basic.Main.AddChapterMarker"), + [](void *, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + obs_frontend_recording_add_chapter(nullptr); + }, + this); + LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker"); + + replayBufHotkeys = + obs_hotkey_pair_register_frontend("OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), + "OBSBasic.StopReplayBuffer", Str("Basic.Main.StopReplayBuffer"), + MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(), + basic.StartReplayBuffer, "Starting replay buffer"), + MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(), + basic.StopReplayBuffer, "Stopping replay buffer"), + this, this); + LoadHotkeyPair(replayBufHotkeys, "OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer"); + + if (vcamEnabled) { + vcamHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.StartVirtualCam", Str("Basic.Main.StartVirtualCam"), "OBSBasic.StopVirtualCam", + Str("Basic.Main.StopVirtualCam"), + MAKE_CALLBACK(!basic.outputHandler->VirtualCamActive(), basic.StartVirtualCam, + "Starting virtual camera"), + MAKE_CALLBACK(basic.outputHandler->VirtualCamActive(), basic.StopVirtualCam, + "Stopping virtual camera"), + this, this); + LoadHotkeyPair(vcamHotkeys, "OBSBasic.StartVirtualCam", "OBSBasic.StopVirtualCam"); + } + + togglePreviewHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreview", Str("Basic.Main.PreviewConextMenu.Enable"), "OBSBasic.DisablePreview", + Str("Basic.Main.Preview.Disable"), + MAKE_CALLBACK(!basic.previewEnabled, basic.EnablePreview, "Enabling preview"), + MAKE_CALLBACK(basic.previewEnabled, basic.DisablePreview, "Disabling preview"), this, this); + LoadHotkeyPair(togglePreviewHotkeys, "OBSBasic.EnablePreview", "OBSBasic.DisablePreview"); + + togglePreviewProgramHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.EnablePreviewProgram", Str("Basic.EnablePreviewProgramMode"), + "OBSBasic.DisablePreviewProgram", Str("Basic.DisablePreviewProgramMode"), + MAKE_CALLBACK(!basic.IsPreviewProgramMode(), basic.EnablePreviewProgram, "Enabling preview program"), + MAKE_CALLBACK(basic.IsPreviewProgramMode(), basic.DisablePreviewProgram, "Disabling preview program"), + this, this); + LoadHotkeyPair(togglePreviewProgramHotkeys, "OBSBasic.EnablePreviewProgram", "OBSBasic.DisablePreviewProgram", + "OBSBasic.TogglePreviewProgram"); + + contextBarHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.ShowContextBar", Str("Basic.Main.ShowContextBar"), "OBSBasic.HideContextBar", + Str("Basic.Main.HideContextBar"), + MAKE_CALLBACK(!basic.ui->contextContainer->isVisible(), basic.ShowContextBar, "Showing Context Bar"), + MAKE_CALLBACK(basic.ui->contextContainer->isVisible(), basic.HideContextBar, "Hiding Context Bar"), + this, this); + LoadHotkeyPair(contextBarHotkeys, "OBSBasic.ShowContextBar", "OBSBasic.HideContextBar"); +#undef MAKE_CALLBACK + + auto transition = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "TransitionClicked", + Qt::QueuedConnection); + }; + + transitionHotkey = obs_hotkey_register_frontend("OBSBasic.Transition", Str("Transition"), transition, this); + LoadHotkey(transitionHotkey, "OBSBasic.Transition"); + + auto resetStats = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ResetStatsHotkey", + Qt::QueuedConnection); + }; + + statsHotkey = + obs_hotkey_register_frontend("OBSBasic.ResetStats", Str("Basic.Stats.ResetStats"), resetStats, this); + LoadHotkey(statsHotkey, "OBSBasic.ResetStats"); + + auto screenshot = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "Screenshot", Qt::QueuedConnection); + }; + + screenshotHotkey = obs_hotkey_register_frontend("OBSBasic.Screenshot", Str("Screenshot"), screenshot, this); + LoadHotkey(screenshotHotkey, "OBSBasic.Screenshot"); + + auto screenshotSource = [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + if (pressed) + QMetaObject::invokeMethod(static_cast(data), "ScreenshotSelectedSource", + Qt::QueuedConnection); + }; + + sourceScreenshotHotkey = obs_hotkey_register_frontend("OBSBasic.SelectedSourceScreenshot", + Str("Screenshot.SourceHotkey"), screenshotSource, this); + LoadHotkey(sourceScreenshotHotkey, "OBSBasic.SelectedSourceScreenshot"); +} + +void OBSBasic::ClearHotkeys() +{ + obs_hotkey_pair_unregister(streamingHotkeys); + obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); + obs_hotkey_unregister(splitFileHotkey); + obs_hotkey_unregister(addChapterHotkey); + obs_hotkey_pair_unregister(replayBufHotkeys); + obs_hotkey_pair_unregister(vcamHotkeys); + obs_hotkey_pair_unregister(togglePreviewHotkeys); + obs_hotkey_pair_unregister(contextBarHotkeys); + obs_hotkey_pair_unregister(togglePreviewProgramHotkeys); + obs_hotkey_unregister(forceStreamingStopHotkey); + obs_hotkey_unregister(transitionHotkey); + obs_hotkey_unregister(statsHotkey); + obs_hotkey_unregister(screenshotHotkey); + obs_hotkey_unregister(sourceScreenshotHotkey); +} + +OBSBasic::~OBSBasic() +{ + /* clear out UI event queue */ + QApplication::sendPostedEvents(nullptr); + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + if (updateCheckThread && updateCheckThread->isRunning()) + updateCheckThread->wait(); + + if (patronJsonThread && patronJsonThread->isRunning()) + patronJsonThread->wait(); + + delete screenshotData; + delete previewProjector; + delete studioProgramProjector; + delete previewProjectorSource; + delete previewProjectorMain; + delete sourceProjector; + delete sceneProjectorMenu; + delete scaleFilteringMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + delete perSceneTransitionMenu; + delete shortcutFilter; + delete trayMenu; + delete programOptions; + delete program; + + /* XXX: any obs data must be released before calling obs_shutdown. + * currently, we can't automate this with C++ RAII because of the + * delicate nature of obs_shutdown needing to be freed before the UI + * can be freed, and we have no control over the destruction order of + * the Qt UI stuff, so we have to manually clear any references to + * libobs. */ + delete cpuUsageTimer; + os_cpu_usage_info_destroy(cpuUsageInfo); + + obs_hotkey_set_callback_routing_func(nullptr, nullptr); + ClearHotkeys(); + + service = nullptr; + outputHandler.reset(); + + delete interaction; + delete properties; + delete filters; + delete transformWindow; + delete advAudioWindow; + delete about; + delete remux; + + obs_display_remove_draw_callback(ui->preview->GetDisplay(), OBSBasic::RenderMain, this); + + obs_enter_graphics(); + gs_vertexbuffer_destroy(box); + gs_vertexbuffer_destroy(boxLeft); + gs_vertexbuffer_destroy(boxTop); + gs_vertexbuffer_destroy(boxRight); + gs_vertexbuffer_destroy(boxBottom); + gs_vertexbuffer_destroy(circle); + gs_vertexbuffer_destroy(actionSafeMargin); + gs_vertexbuffer_destroy(graphicsSafeMargin); + gs_vertexbuffer_destroy(fourByThreeSafeMargin); + gs_vertexbuffer_destroy(leftLine); + gs_vertexbuffer_destroy(topLine); + gs_vertexbuffer_destroy(rightLine); + obs_leave_graphics(); + + /* When shutting down, sometimes source references can get in to the + * event queue, and if we don't forcibly process those events they + * won't get processed until after obs_shutdown has been called. I + * really wish there were a more elegant way to deal with this via C++, + * but Qt doesn't use C++ in a normal way, so you can't really rely on + * normal C++ behavior for your data to be freed in the order that you + * expect or want it to. */ + QApplication::sendPostedEvents(nullptr); + + config_set_int(App()->GetAppConfig(), "General", "LastVersion", LIBOBS_API_VER); + config_save_safe(App()->GetAppConfig(), "tmp", nullptr); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewEnabled", previewEnabled); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "AlwaysOnTop", ui->actionAlwaysOnTop->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SceneDuplicationMode", sceneDuplicationMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SwapScenesMode", swapScenesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "EditPropertiesMode", editPropertiesMode); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "PreviewProgramMode", IsPreviewProgramMode()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "DocksLocked", ui->lockDocks->isChecked()); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "SideDocks", ui->sideDocks->isChecked()); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + +#ifdef BROWSER_AVAILABLE + DestroyPanelCookieManager(); + delete cef; + cef = nullptr; +#endif +} + +void OBSBasic::SaveProjectNow() +{ + if (disableSaving) + return; + + projectChanged = true; + SaveProjectDeferred(); +} + +void OBSBasic::SaveProject() +{ + if (disableSaving) + return; + + projectChanged = true; + QMetaObject::invokeMethod(this, "SaveProjectDeferred", Qt::QueuedConnection); +} + +void OBSBasic::SaveProjectDeferred() +{ + if (disableSaving) + return; + + if (!projectChanged) + return; + + projectChanged = false; + + try { + const OBSSceneCollection ¤tCollection = GetCurrentSceneCollection(); + + Save(currentCollection.collectionFile.u8string().c_str()); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +OBSSource OBSBasic::GetProgramSource() +{ + return OBSGetStrongRef(programScene); +} + +OBSScene OBSBasic::GetCurrentScene() +{ + return currentScene.load(); +} + +OBSSceneItem OBSBasic::GetSceneItem(QListWidgetItem *item) +{ + return item ? GetOBSRef(item) : nullptr; +} + +OBSSceneItem OBSBasic::GetCurrentSceneItem() +{ + return ui->sources->Get(GetTopSelectedSourceItem()); +} + +void OBSBasic::UpdatePreviewScalingMenu() +{ + bool fixedScaling = ui->preview->IsFixedScaling(); + float scalingAmount = ui->preview->GetScalingAmount(); + if (!fixedScaling) { + ui->actionScaleWindow->setChecked(true); + ui->actionScaleCanvas->setChecked(false); + ui->actionScaleOutput->setChecked(false); + return; + } + + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->actionScaleWindow->setChecked(false); + ui->actionScaleCanvas->setChecked(scalingAmount == 1.0f); + ui->actionScaleOutput->setChecked(scalingAmount == float(ovi.output_width) / float(ovi.base_width)); +} + +void OBSBasic::CreateInteractionWindow(obs_source_t *source) +{ + bool closed = true; + if (interaction) + closed = interaction->close(); + + if (!closed) + return; + + interaction = new OBSBasicInteraction(this, source); + interaction->Init(); + interaction->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreatePropertiesWindow(obs_source_t *source) +{ + bool closed = true; + if (properties) + closed = properties->close(); + + if (!closed) + return; + + properties = new OBSBasicProperties(this, source); + properties->Init(); + properties->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::CreateFiltersWindow(obs_source_t *source) +{ + bool closed = true; + if (filters) + closed = filters->close(); + + if (!closed) + return; + + filters = new OBSBasicFilters(this, source); + filters->Init(); + filters->setAttribute(Qt::WA_DeleteOnClose, true); +} + +/* Qt callbacks for invokeMethod */ + +void OBSBasic::AddScene(OBSSource source) +{ + const char *name = obs_source_get_name(source); + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *item = new QListWidgetItem(QT_UTF8(name)); + SetOBSRef(item, OBSScene(scene)); + ui->scenes->insertItem(ui->scenes->currentRow() + 1, item); + + obs_hotkey_register_source( + source, "OBSBasic.SelectScene", Str("Basic.Hotkeys.SelectScene"), + [](void *data, obs_hotkey_id, obs_hotkey_t *, bool pressed) { + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + auto potential_source = static_cast(data); + OBSSourceAutoRelease source = obs_source_get_ref(potential_source); + if (source && pressed) + main->SetCurrentScene(source.Get()); + }, + static_cast(source)); + + signal_handler_t *handler = obs_source_get_signal_handler(source); + + SignalContainer container; + container.ref = scene; + container.handlers.assign({ + std::make_shared(handler, "item_add", OBSBasic::SceneItemAdded, this), + std::make_shared(handler, "reorder", OBSBasic::SceneReordered, this), + std::make_shared(handler, "refresh", OBSBasic::SceneRefreshed, this), + }); + + item->setData(static_cast(QtDataRole::OBSSignals), QVariant::fromValue(container)); + + /* if the scene already has items (a duplicated scene) add them */ + auto addSceneItem = [this](obs_sceneitem_t *item) { + AddSceneItem(item); + }; + + using addSceneItem_t = decltype(addSceneItem); + + obs_scene_enum_items( + scene, + [](obs_scene_t *, obs_sceneitem_t *item, void *param) { + addSceneItem_t *func; + func = reinterpret_cast(param); + (*func)(item); + return true; + }, + &addSceneItem); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *source = obs_scene_get_source(scene); + blog(LOG_INFO, "User added scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::RemoveScene(OBSSource source) +{ + obs_scene_t *scene = obs_scene_from_source(source); + + QListWidgetItem *sel = nullptr; + int count = ui->scenes->count(); + + for (int i = 0; i < count; i++) { + auto item = ui->scenes->item(i); + auto cur_scene = GetOBSRef(item); + if (cur_scene != scene) + continue; + + sel = item; + break; + } + + if (sel != nullptr) { + if (sel == ui->scenes->currentItem()) + ui->sources->Clear(); + delete sel; + } + + SaveProject(); + + if (!disableSaving) { + blog(LOG_INFO, "User Removed scene '%s'", obs_source_get_name(source)); + + OBSProjector::UpdateMultiviewProjectors(); + } + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +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)); + + return true; +} + +void OBSBasic::AddSceneItem(OBSSceneItem item) +{ + obs_scene_t *scene = obs_sceneitem_get_scene(item); + + if (GetCurrentScene() == scene) + ui->sources->Add(item); + + SaveProject(); + + if (!disableSaving) { + obs_source_t *sceneSource = obs_scene_get_source(scene); + obs_source_t *itemSource = obs_sceneitem_get_source(item); + blog(LOG_INFO, "User added source '%s' (%s) to scene '%s'", obs_source_get_name(itemSource), + obs_source_get_id(itemSource), obs_source_get_name(sceneSource)); + + obs_scene_enum_items(scene, select_one, (obs_sceneitem_t *)item); + } +} + +static void RenameListValues(QListWidget *listWidget, const QString &newName, const QString &prevName) +{ + QList items = listWidget->findItems(prevName, Qt::MatchExactly); + + for (int i = 0; i < items.count(); i++) + items[i]->setText(newName); +} + +void OBSBasic::RenameSources(OBSSource source, QString newName, QString prevName) +{ + RenameListValues(ui->scenes, newName, prevName); + + if (vcamConfig.type == VCamOutputType::SourceOutput && prevName == QString::fromStdString(vcamConfig.source)) + vcamConfig.source = newName.toStdString(); + if (vcamConfig.type == VCamOutputType::SceneOutput && prevName == QString::fromStdString(vcamConfig.scene)) + vcamConfig.scene = newName.toStdString(); + + SaveProject(); + + obs_scene_t *scene = obs_scene_from_source(source); + if (scene) + OBSProjector::UpdateMultiviewProjectors(); + + UpdateContextBar(); + UpdatePreviewProgramIndicators(); +} + +void OBSBasic::ClearContextBar() +{ + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + delete la->widget(); + ui->emptySpace->layout()->removeItem(la); + } +} + +void OBSBasic::UpdateContextBarVisibility() +{ + int width = ui->centralwidget->size().width(); + + ContextBarSize contextBarSizeNew; + if (width >= 740) { + contextBarSizeNew = ContextBarSize_Normal; + } else if (width >= 600) { + contextBarSizeNew = ContextBarSize_Reduced; + } else { + contextBarSizeNew = ContextBarSize_Minimized; + } + + if (contextBarSize == contextBarSizeNew) + return; + + contextBarSize = contextBarSizeNew; + UpdateContextBarDeferred(); +} + +static bool is_network_media_source(obs_source_t *source, const char *id) +{ + if (strcmp(id, "ffmpeg_source") != 0) + return false; + + OBSDataAutoRelease s = obs_source_get_settings(source); + bool is_local_file = obs_data_get_bool(s, "is_local_file"); + + return !is_local_file; +} + +void OBSBasic::UpdateContextBarDeferred(bool force) +{ + QMetaObject::invokeMethod(this, "UpdateContextBar", Qt::QueuedConnection, Q_ARG(bool, force)); +} + +void OBSBasic::SourceToolBarActionsSetEnabled() +{ + bool enable = false; + bool disableProps = false; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + OBSSource source = obs_sceneitem_get_source(item); + disableProps = !obs_source_configurable(source); + + enable = true; + } + + if (disableProps) + ui->actionSourceProperties->setEnabled(false); + else + ui->actionSourceProperties->setEnabled(enable); + + ui->actionRemoveSource->setEnabled(enable); + ui->actionSourceUp->setEnabled(enable); + ui->actionSourceDown->setEnabled(enable); + + RefreshToolBarStyling(ui->sourcesToolbar); +} + +void OBSBasic::UpdateContextBar(bool force) +{ + SourceToolBarActionsSetEnabled(); + + if (!ui->contextContainer->isVisible() && !force) + return; + + OBSSceneItem item = GetCurrentSceneItem(); + + if (item) { + obs_source_t *source = obs_sceneitem_get_source(item); + + bool updateNeeded = true; + QLayoutItem *la = ui->emptySpace->layout()->itemAt(0); + if (la) { + if (SourceToolbar *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } else if (MediaControls *toolbar = dynamic_cast(la->widget())) { + if (toolbar->GetSource() == source) + updateNeeded = false; + } + } + + const char *id = obs_source_get_unversioned_id(source); + uint32_t flags = obs_source_get_output_flags(source); + + ui->sourceInteractButton->setVisible(flags & OBS_SOURCE_INTERACTION); + + if (contextBarSize >= ContextBarSize_Reduced && (updateNeeded || force)) { + ClearContextBar(); + if (flags & OBS_SOURCE_CONTROLLABLE_MEDIA) { + if (!is_network_media_source(source, id)) { + MediaControls *mediaControls = new MediaControls(ui->emptySpace); + mediaControls->SetSource(source); + + ui->emptySpace->layout()->addWidget(mediaControls); + } + } else if (strcmp(id, "browser_source") == 0) { + BrowserToolbar *c = new BrowserToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_input_capture") == 0 || + strcmp(id, "wasapi_output_capture") == 0 || + strcmp(id, "coreaudio_input_capture") == 0 || + strcmp(id, "coreaudio_output_capture") == 0 || + strcmp(id, "pulse_input_capture") == 0 || strcmp(id, "pulse_output_capture") == 0 || + strcmp(id, "alsa_input_capture") == 0) { + AudioCaptureToolbar *c = new AudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "wasapi_process_output_capture") == 0) { + ApplicationAudioCaptureToolbar *c = + new ApplicationAudioCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "window_capture") == 0 || strcmp(id, "xcomposite_input") == 0) { + WindowCaptureToolbar *c = new WindowCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "monitor_capture") == 0 || strcmp(id, "display_capture") == 0 || + strcmp(id, "xshm_input") == 0) { + DisplayCaptureToolbar *c = new DisplayCaptureToolbar(ui->emptySpace, source); + c->Init(); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "dshow_input") == 0) { + DeviceCaptureToolbar *c = new DeviceCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "game_capture") == 0) { + GameCaptureToolbar *c = new GameCaptureToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "image_source") == 0) { + ImageSourceToolbar *c = new ImageSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "color_source") == 0) { + ColorSourceToolbar *c = new ColorSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + + } else if (strcmp(id, "text_ft2_source") == 0 || strcmp(id, "text_gdiplus") == 0) { + TextSourceToolbar *c = new TextSourceToolbar(ui->emptySpace, source); + ui->emptySpace->layout()->addWidget(c); + } + } else if (contextBarSize == ContextBarSize_Minimized) { + ClearContextBar(); + } + + QIcon icon; + + if (strcmp(id, "scene") == 0) + icon = GetSceneIcon(); + else if (strcmp(id, "group") == 0) + icon = GetGroupIcon(); + else + icon = GetSourceIcon(id); + + QPixmap pixmap = icon.pixmap(QSize(16, 16)); + ui->contextSourceIcon->setPixmap(pixmap); + ui->contextSourceIconSpacer->hide(); + ui->contextSourceIcon->show(); + + const char *name = obs_source_get_name(source); + ui->contextSourceLabel->setText(name); + + ui->sourceFiltersButton->setEnabled(true); + ui->sourcePropertiesButton->setEnabled(obs_source_configurable(source)); + } else { + ClearContextBar(); + ui->contextSourceIcon->hide(); + ui->contextSourceIconSpacer->show(); + ui->contextSourceLabel->setText(QTStr("ContextBar.NoSelectedSource")); + + ui->sourceFiltersButton->setEnabled(false); + ui->sourcePropertiesButton->setEnabled(false); + ui->sourceInteractButton->setVisible(false); + } + + if (contextBarSize == ContextBarSize_Normal) { + ui->sourcePropertiesButton->setText(QTStr("Properties")); + ui->sourceFiltersButton->setText(QTStr("Filters")); + ui->sourceInteractButton->setText(QTStr("Interact")); + } else { + ui->sourcePropertiesButton->setText(""); + ui->sourceFiltersButton->setText(""); + ui->sourceInteractButton->setText(""); + } +} + +static inline bool SourceMixerHidden(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool hidden = obs_data_get_bool(priv_settings, "mixer_hidden"); + + return hidden; +} + +static inline void SetSourceMixerHidden(obs_source_t *source, bool hidden) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "mixer_hidden", hidden); +} + +void OBSBasic::GetAudioSourceFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreateFiltersWindow(source); +} + +void OBSBasic::GetAudioSourceProperties() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + CreatePropertiesWindow(source); +} + +void OBSBasic::HideAudioControl() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } +} + +void OBSBasic::UnhideAllAudioControls() +{ + auto UnhideAudioMixer = [this](obs_source_t *source) /* -- */ + { + if (!obs_source_active(source)) + return true; + if (!SourceMixerHidden(source)) + return true; + + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + return true; + }; + + using UnhideAudioMixer_t = decltype(UnhideAudioMixer); + + auto PreEnum = [](void *data, obs_source_t *source) -> bool /* -- */ + { + return (*reinterpret_cast(data))(source); + }; + + obs_enum_sources(PreEnum, &UnhideAudioMixer); +} + +void OBSBasic::ToggleHideMixer() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (!SourceMixerHidden(source)) { + SetSourceMixerHidden(source, true); + DeactivateAudioSource(source); + } else { + SetSourceMixerHidden(source, false); + ActivateAudioSource(source); + } +} + +void OBSBasic::MixerRenameSource() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + OBSSource source = vol->GetSource(); + + const char *prevName = obs_source_get_name(source); + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.MixerRename.Title"), + QTStr("Basic.Main.MixerRename.Text"), name, QT_UTF8(prevName)); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + OBSSourceAutoRelease sourceTest = obs_get_source_by_name(name.c_str()); + + if (sourceTest) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + obs_source_set_name(source, name.c_str()); + break; + } +} + +static inline bool SourceVolumeLocked(obs_source_t *source) +{ + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + bool lock = obs_data_get_bool(priv_settings, "volume_locked"); + + return lock; +} + +void OBSBasic::LockVolumeControl(bool lock) +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + OBSDataAutoRelease priv_settings = obs_source_get_private_settings(source); + obs_data_set_bool(priv_settings, "volume_locked", lock); + + vol->EnableSlider(!lock); +} + +void OBSBasic::VolControlContextMenu() +{ + VolControl *vol = reinterpret_cast(sender()); + + /* ------------------- */ + + QAction lockAction(QTStr("LockVolume"), this); + lockAction.setCheckable(true); + lockAction.setChecked(SourceVolumeLocked(vol->GetSource())); + + QAction hideAction(QTStr("Hide"), this); + QAction unhideAllAction(QTStr("UnhideAll"), this); + QAction mixerRenameAction(QTStr("Rename"), this); + + QAction copyFiltersAction(QTStr("Copy.Filters"), this); + QAction pasteFiltersAction(QTStr("Paste.Filters"), this); + + QAction filtersAction(QTStr("Filters"), this); + QAction propertiesAction(QTStr("Properties"), this); + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&hideAction, &QAction::triggered, this, &OBSBasic::HideAudioControl, Qt::DirectConnection); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + connect(&lockAction, &QAction::toggled, this, &OBSBasic::LockVolumeControl, Qt::DirectConnection); + connect(&mixerRenameAction, &QAction::triggered, this, &OBSBasic::MixerRenameSource, Qt::DirectConnection); + + connect(©FiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerCopyFilters, Qt::DirectConnection); + connect(&pasteFiltersAction, &QAction::triggered, this, &OBSBasic::AudioMixerPasteFilters, + Qt::DirectConnection); + + connect(&filtersAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceFilters, Qt::DirectConnection); + connect(&propertiesAction, &QAction::triggered, this, &OBSBasic::GetAudioSourceProperties, + Qt::DirectConnection); + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + hideAction.setProperty("volControl", QVariant::fromValue(vol)); + lockAction.setProperty("volControl", QVariant::fromValue(vol)); + mixerRenameAction.setProperty("volControl", QVariant::fromValue(vol)); + + copyFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + pasteFiltersAction.setProperty("volControl", QVariant::fromValue(vol)); + + filtersAction.setProperty("volControl", QVariant::fromValue(vol)); + propertiesAction.setProperty("volControl", QVariant::fromValue(vol)); + + /* ------------------- */ + + copyFiltersAction.setEnabled(obs_source_filter_count(vol->GetSource()) > 0); + pasteFiltersAction.setEnabled(!obs_weak_source_expired(copyFiltersSource)); + + QMenu popup; + vol->SetContextMenu(&popup); + popup.addAction(&lockAction); + popup.addSeparator(); + popup.addAction(&unhideAllAction); + popup.addAction(&hideAction); + popup.addAction(&mixerRenameAction); + popup.addSeparator(); + popup.addAction(©FiltersAction); + popup.addAction(&pasteFiltersAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&filtersAction); + popup.addAction(&propertiesAction); + popup.addAction(&advPropAction); + + // toggleControlLayoutAction deletes and re-creates the volume controls + // meaning that "vol" would be pointing to freed memory. + if (popup.exec(QCursor::pos()) != &toggleControlLayoutAction) + vol->SetContextMenu(nullptr); +} + +void OBSBasic::on_hMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::on_vMixerScrollArea_customContextMenuRequested() +{ + StackedMixerAreaContextMenuRequested(); +} + +void OBSBasic::StackedMixerAreaContextMenuRequested() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + + QAction advPropAction(QTStr("Basic.MainMenu.Edit.AdvAudio"), this); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + + /* ------------------- */ + + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + connect(&advPropAction, &QAction::triggered, this, &OBSBasic::on_actionAdvAudioProperties_triggered, + Qt::DirectConnection); + + /* ------------------- */ + + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + /* ------------------- */ + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.addSeparator(); + popup.addAction(&advPropAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::ToggleMixerLayout(bool vertical) +{ + if (vertical) { + ui->stackedMixerArea->setMinimumSize(180, 220); + ui->stackedMixerArea->setCurrentIndex(1); + } else { + ui->stackedMixerArea->setMinimumSize(220, 0); + ui->stackedMixerArea->setCurrentIndex(0); + } +} + +void OBSBasic::ToggleVolControlLayout() +{ + bool vertical = !config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl", vertical); + ToggleMixerLayout(vertical); + + // We need to store it so we can delete current and then add + // at the right order + vector sources; + for (size_t i = 0; i != volumes.size(); i++) + sources.emplace_back(volumes[i]->GetSource()); + + ClearVolumeControls(); + + for (const auto &source : sources) + ActivateAudioSource(source); +} + +void OBSBasic::ActivateAudioSource(OBSSource source) +{ + if (SourceMixerHidden(source)) + return; + if (!obs_source_active(source)) + return; + if (!obs_source_audio_active(source)) + return; + + bool vertical = config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl"); + VolControl *vol = new VolControl(source, true, vertical); + + vol->EnableSlider(!SourceVolumeLocked(source)); + + double meterDecayRate = config_get_double(activeConfiguration, "Audio", "MeterDecayRate"); + vol->SetMeterDecayRate(meterDecayRate); + + uint32_t peakMeterTypeIdx = config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); + + enum obs_peak_meter_type peakMeterType; + switch (peakMeterTypeIdx) { + case 0: + peakMeterType = SAMPLE_PEAK_METER; + break; + case 1: + peakMeterType = TRUE_PEAK_METER; + break; + default: + peakMeterType = SAMPLE_PEAK_METER; + break; + } + + vol->setPeakMeterType(peakMeterType); + + vol->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(vol, &QWidget::customContextMenuRequested, this, &OBSBasic::VolControlContextMenu); + connect(vol, &VolControl::ConfigClicked, this, &OBSBasic::VolControlContextMenu); + + InsertQObjectByName(volumes, vol); + + for (auto volume : volumes) { + if (vertical) + ui->vVolControlLayout->addWidget(volume); + else + ui->hVolControlLayout->addWidget(volume); + } +} + +void OBSBasic::DeactivateAudioSource(OBSSource source) +{ + for (size_t i = 0; i < volumes.size(); i++) { + if (volumes[i]->GetSource() == source) { + delete volumes[i]; + volumes.erase(volumes.begin() + i); + break; + } + } +} + +bool OBSBasic::QueryRemoveSource(obs_source_t *source) +{ + if (obs_source_get_type(source) == OBS_SOURCE_TYPE_SCENE && !obs_source_is_group(source)) { + int count = ui->scenes->count(); + + if (count == 1) { + OBSMessageBox::information(this, QTStr("FinalScene.Title"), QTStr("FinalScene.Text")); + return false; + } + } + + const char *name = obs_source_get_name(source); + + QString text = QTStr("ConfirmRemove.Text").arg(QT_UTF8(name)); + + QMessageBox remove_source(this); + remove_source.setText(text); + QPushButton *Yes = remove_source.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_source.setDefaultButton(Yes); + remove_source.addButton(QTStr("No"), QMessageBox::NoRole); + remove_source.setIcon(QMessageBox::Question); + remove_source.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_source.exec(); + + return Yes == remove_source.clickedButton(); +} + +#define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ + +void OBSBasic::TimedCheckForUpdates() +{ + if (App()->IsUpdaterDisabled()) + return; + if (!config_get_bool(App()->GetAppConfig(), "General", "EnableAutoUpdates")) + return; + +#if defined(ENABLE_SPARKLE_UPDATER) + CheckForUpdates(false); +#elif _WIN32 + long long lastUpdate = config_get_int(App()->GetAppConfig(), "General", "LastUpdateCheck"); + uint32_t lastVersion = config_get_int(App()->GetAppConfig(), "General", "LastVersion"); + + if (lastVersion < LIBOBS_API_VER) { + lastUpdate = 0; + config_set_int(App()->GetAppConfig(), "General", "LastUpdateCheck", 0); + } + + long long t = (long long)time(nullptr); + long long secs = t - lastUpdate; + + if (secs > UPDATE_CHECK_INTERVAL) + CheckForUpdates(false); +#endif +} + +void OBSBasic::CheckForUpdates(bool manualUpdate) +{ +#if _WIN32 + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); + updateCheckThread->start(); +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#else + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + +void OBSBasic::updateCheckFinished() +{ + ui->actionCheckForUpdates->setEnabled(true); + ui->actionRepair->setEnabled(true); +} + +void OBSBasic::DuplicateSelectedScene() +{ + OBSScene curScene = GetCurrentScene(); + + if (!curScene) + return; + + OBSSource curSceneSource = obs_scene_get_source(curScene); + QString format{obs_source_get_name(curSceneSource)}; + format += " %1"; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + for (;;) { + string name; + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + if (!accepted) + return; + + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + obs_source_t *source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + obs_source_release(source); + continue; + } + + OBSSceneAutoRelease scene = obs_scene_duplicate(curScene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + + auto undo = [](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_source_remove(source); + }; + + auto redo = [this, name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_name(data.c_str()); + obs_scene_t *scene = obs_scene_from_source(source); + scene = obs_scene_duplicate(scene, name.c_str(), OBS_SCENE_DUP_REFS); + source = obs_scene_get_source(scene); + SetCurrentScene(source.Get(), true); + }; + + undo_s.add_action(QTStr("Undo.Scene.Duplicate").arg(obs_source_get_name(source)), undo, redo, + obs_source_get_name(source), obs_source_get_name(obs_scene_get_source(curScene))); + + break; + } +} + +static bool save_undo_source_enum(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *p) +{ + obs_source_t *source = obs_sceneitem_get_source(item); + if (obs_obj_is_private(source) && !obs_source_removed(source)) + return true; + + obs_data_array_t *array = (obs_data_array_t *)p; + + /* check if the source is already stored in the array */ + const char *name = obs_source_get_name(source); + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease sourceData = obs_data_array_item(array, i); + if (strcmp(name, obs_data_get_string(sourceData, "name")) == 0) + return true; + } + + if (obs_source_is_group(source)) + obs_scene_enum_items(obs_group_from_source(source), save_undo_source_enum, p); + + OBSDataAutoRelease source_data = obs_save_source(source); + obs_data_array_push_back(array, source_data); + return true; +} + +static inline void RemoveSceneAndReleaseNested(obs_source_t *source) +{ + obs_source_remove(source); + auto cb = [](void *, obs_source_t *source) { + if (strcmp(obs_source_get_id(source), "scene") == 0) + obs_scene_prune_sources(obs_scene_from_source(source)); + return true; + }; + obs_enum_scenes(cb, NULL); +} + +void OBSBasic::RemoveSelectedScene() +{ + OBSScene scene = GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + + if (!source || !QueryRemoveSource(source)) { + return; + } + + /* ------------------------------ */ + /* save all sources in scene */ + + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_array_create(); + + obs_scene_enum_items(scene, save_undo_source_enum, sources_in_deleted_scene); + + OBSDataAutoRelease scene_data = obs_save_source(source); + obs_data_array_push_back(sources_in_deleted_scene, scene_data); + + /* ----------------------------------------------- */ + /* save all scenes and groups the scene is used in */ + + OBSDataArrayAutoRelease scene_used_in_other_scenes = obs_data_array_create(); + + struct other_scenes_cb_data { + obs_source_t *oldScene; + obs_data_array_t *scene_used_in_other_scenes; + } other_scenes_cb_data; + other_scenes_cb_data.oldScene = source; + other_scenes_cb_data.scene_used_in_other_scenes = scene_used_in_other_scenes; + + auto other_scenes_cb = [](void *data_ptr, obs_source_t *scene) { + struct other_scenes_cb_data *data = (struct other_scenes_cb_data *)data_ptr; + if (strcmp(obs_source_get_name(scene), obs_source_get_name(data->oldScene)) == 0) + return true; + obs_sceneitem_t *item = obs_scene_find_source(obs_group_or_scene_from_source(scene), + obs_source_get_name(data->oldScene)); + if (item) { + OBSDataAutoRelease scene_data = + obs_save_source(obs_scene_get_source(obs_sceneitem_get_scene(item))); + obs_data_array_push_back(data->scene_used_in_other_scenes, scene_data); + } + return true; + }; + obs_enum_scenes(other_scenes_cb, &other_scenes_cb_data); + + /* --------------------------- */ + /* undo/redo */ + + auto undo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease sources_in_deleted_scene = obs_data_get_array(base, "sources_in_deleted_scene"); + OBSDataArrayAutoRelease scene_used_in_other_scenes = + obs_data_get_array(base, "scene_used_in_other_scenes"); + int savedIndex = (int)obs_data_get_int(base, "index"); + std::vector sources; + + /* create missing sources */ + size_t count = obs_data_array_count(sources_in_deleted_scene); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(sources_in_deleted_scene, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) { + source = obs_load_source(data); + sources.push_back(source.Get()); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + /* Add scene to scenes and groups it was nested in */ + for (size_t i = 0; i < obs_data_array_count(scene_used_in_other_scenes); i++) { + OBSDataAutoRelease data = obs_data_array_item(scene_used_in_other_scenes, i); + const char *name = obs_data_get_string(data, "name"); + OBSSourceAutoRelease source = obs_get_source_by_name(name); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataArrayAutoRelease items = obs_data_get_array(settings, "items"); + + /* Clear scene, but keep a reference to all sources in the scene to make sure they don't get destroyed */ + std::vector existing_sources; + auto cb = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + std::vector *existing = (std::vector *)data; + OBSSource source = obs_sceneitem_get_source(item); + obs_sceneitem_remove(item); + existing->push_back(source); + return true; + }; + obs_scene_enum_items(obs_group_or_scene_from_source(source), cb, (void *)&existing_sources); + + /* Re-add sources to the scene */ + obs_sceneitems_add(obs_group_or_scene_from_source(source), items); + } + + obs_source_t *scene_source = sources.back(); + OBSScene scene = obs_scene_from_source(scene_source); + SetCurrentScene(scene, true); + + /* set original index in list box */ + ui->scenes->blockSignals(true); + int curIndex = ui->scenes->currentRow(); + QListWidgetItem *item = ui->scenes->takeItem(curIndex); + ui->scenes->insertItem(savedIndex, item); + ui->scenes->setCurrentRow(savedIndex); + currentScene = scene.Get(); + ui->scenes->blockSignals(false); + }; + + auto redo = [](const std::string &name) { + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + RemoveSceneAndReleaseNested(source); + }; + + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_array(data, "sources_in_deleted_scene", sources_in_deleted_scene); + obs_data_set_array(data, "scene_used_in_other_scenes", scene_used_in_other_scenes); + obs_data_set_int(data, "index", ui->scenes->currentRow()); + + const char *scene_name = obs_source_get_name(source); + undo_s.add_action(QTStr("Undo.Delete").arg(scene_name), undo, redo, obs_data_get_json(data), scene_name); + + /* --------------------------- */ + /* remove */ + + RemoveSceneAndReleaseNested(source); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::ReorderSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->ReorderItems(); + SaveProject(); +} + +void OBSBasic::RefreshSources(OBSScene scene) +{ + if (scene != GetCurrentScene() || ui->sources->IgnoreReorder()) + return; + + ui->sources->RefreshItems(); + SaveProject(); +} + +/* OBS Callbacks */ + +void OBSBasic::SceneReordered(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "ReorderSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneRefreshed(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_scene_t *scene = (obs_scene_t *)calldata_ptr(params, "scene"); + + QMetaObject::invokeMethod(window, "RefreshSources", Q_ARG(OBSScene, OBSScene(scene))); +} + +void OBSBasic::SceneItemAdded(void *data, calldata_t *params) +{ + OBSBasic *window = static_cast(data); + + obs_sceneitem_t *item = (obs_sceneitem_t *)calldata_ptr(params, "item"); + + QMetaObject::invokeMethod(window, "AddSceneItem", Q_ARG(OBSSceneItem, OBSSceneItem(item))); +} + +void OBSBasic::SourceCreated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "AddScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRemoved(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_scene_from_source(source) != NULL) + QMetaObject::invokeMethod(static_cast(data), "RemoveScene", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + uint32_t flags = obs_source_get_output_flags(source); + + if (flags & OBS_SOURCE_AUDIO) + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioActivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + + if (obs_source_active(source)) + QMetaObject::invokeMethod(static_cast(data), "ActivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceAudioDeactivated(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + QMetaObject::invokeMethod(static_cast(data), "DeactivateAudioSource", + Q_ARG(OBSSource, OBSSource(source))); +} + +void OBSBasic::SourceRenamed(void *data, calldata_t *params) +{ + obs_source_t *source = (obs_source_t *)calldata_ptr(params, "source"); + const char *newName = calldata_string(params, "new_name"); + const char *prevName = calldata_string(params, "prev_name"); + + QMetaObject::invokeMethod(static_cast(data), "RenameSources", Q_ARG(OBSSource, source), + Q_ARG(QString, QT_UTF8(newName)), Q_ARG(QString, QT_UTF8(prevName))); + + blog(LOG_INFO, "Source '%s' renamed to '%s'", prevName, newName); +} + +void OBSBasic::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 OBSBasic::RenderMain(void *data, uint32_t, uint32_t) +{ + GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_DEFAULT, "RenderMain"); + + OBSBasic *window = static_cast(data); + obs_video_info ovi; + + obs_get_video_info(&ovi); + + window->previewCX = int(window->previewScale * float(ovi.base_width)); + window->previewCY = int(window->previewScale * float(ovi.base_height)); + + gs_viewport_push(); + gs_projection_push(); + + obs_display_t *display = window->ui->preview->GetDisplay(); + uint32_t width, height; + obs_display_size(display, &width, &height); + float right = float(width) - window->previewX; + float bottom = float(height) - window->previewY; + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + + window->ui->preview->DrawOverflow(); + + /* --------------------------------------- */ + + gs_ortho(0.0f, float(ovi.base_width), 0.0f, float(ovi.base_height), -100.0f, 100.0f); + gs_set_viewport(window->previewX, window->previewY, window->previewCX, window->previewCY); + + if (window->IsPreviewProgramMode()) { + window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height)); + + OBSScene scene = window->GetCurrentScene(); + obs_source_t *source = obs_scene_get_source(scene); + if (source) + obs_source_video_render(source); + } else { + obs_render_main_texture_src_color_only(); + } + gs_load_vertexbuffer(nullptr); + + /* --------------------------------------- */ + + gs_ortho(-window->previewX, right, -window->previewY, bottom, -100.0f, 100.0f); + gs_reset_viewport(); + + uint32_t targetCX = window->previewCX; + uint32_t targetCY = window->previewCY; + + if (window->drawSafeAreas) { + RenderSafeAreas(window->actionSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->graphicsSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->fourByThreeSafeMargin, targetCX, targetCY); + RenderSafeAreas(window->leftLine, targetCX, targetCY); + RenderSafeAreas(window->topLine, targetCX, targetCY); + RenderSafeAreas(window->rightLine, targetCX, targetCY); + } + + window->ui->preview->DrawSceneEditing(); + + if (window->drawSpacingHelpers) + window->ui->preview->DrawSpacingHelpers(); + + /* --------------------------------------- */ + + gs_projection_pop(); + gs_viewport_pop(); + + GS_DEBUG_MARKER_END(); +} + +/* Main class functions */ + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} + +int OBSBasic::GetTransitionDuration() +{ + return ui->transitionDuration->value(); +} + +bool OBSBasic::Active() const +{ + if (!outputHandler) + return false; + return outputHandler->Active(); +} + +#ifdef _WIN32 +#define IS_WIN32 1 +#else +#define IS_WIN32 0 +#endif + +static inline int AttemptToResetVideo(struct obs_video_info *ovi) +{ + return obs_reset_video(ovi); +} + +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) +{ + const char *scaleTypeStr = config_get_string(activeConfiguration, "Video", "ScaleType"); + + if (astrcmpi(scaleTypeStr, "bilinear") == 0) + return OBS_SCALE_BILINEAR; + else if (astrcmpi(scaleTypeStr, "lanczos") == 0) + return OBS_SCALE_LANCZOS; + else if (astrcmpi(scaleTypeStr, "area") == 0) + return OBS_SCALE_AREA; + else + return OBS_SCALE_BICUBIC; +} + +static inline enum video_format GetVideoFormatFromName(const char *name) +{ + if (astrcmpi(name, "I420") == 0) + return VIDEO_FORMAT_I420; + else if (astrcmpi(name, "NV12") == 0) + return VIDEO_FORMAT_NV12; + else if (astrcmpi(name, "I444") == 0) + return VIDEO_FORMAT_I444; + else if (astrcmpi(name, "I010") == 0) + return VIDEO_FORMAT_I010; + else if (astrcmpi(name, "P010") == 0) + return VIDEO_FORMAT_P010; + else if (astrcmpi(name, "P216") == 0) + return VIDEO_FORMAT_P216; + else if (astrcmpi(name, "P416") == 0) + return VIDEO_FORMAT_P416; +#if 0 //currently unsupported + else if (astrcmpi(name, "YVYU") == 0) + return VIDEO_FORMAT_YVYU; + else if (astrcmpi(name, "YUY2") == 0) + return VIDEO_FORMAT_YUY2; + else if (astrcmpi(name, "UYVY") == 0) + return VIDEO_FORMAT_UYVY; +#endif + else + return VIDEO_FORMAT_BGRA; +} + +static inline enum video_colorspace GetVideoColorSpaceFromName(const char *name) +{ + enum video_colorspace colorspace = VIDEO_CS_SRGB; + if (strcmp(name, "601") == 0) + colorspace = VIDEO_CS_601; + else if (strcmp(name, "709") == 0) + colorspace = VIDEO_CS_709; + else if (strcmp(name, "2100PQ") == 0) + colorspace = VIDEO_CS_2100_PQ; + else if (strcmp(name, "2100HLG") == 0) + colorspace = VIDEO_CS_2100_HLG; + + return colorspace; +} + +void OBSBasic::ResetUI() +{ + bool studioPortraitLayout = config_get_bool(App()->GetUserConfig(), "BasicWindow", "StudioPortraitLayout"); + + if (studioPortraitLayout) + ui->previewLayout->setDirection(QBoxLayout::BottomToTop); + else + ui->previewLayout->setDirection(QBoxLayout::LeftToRight); + + UpdatePreviewProgramIndicators(); +} + +int OBSBasic::ResetVideo() +{ + if (outputHandler && outputHandler->Active()) + return OBS_VIDEO_CURRENTLY_ACTIVE; + + ProfileScope("OBSBasic::ResetVideo"); + + struct obs_video_info ovi; + int ret; + + GetConfigFPS(ovi.fps_num, ovi.fps_den); + + const char *colorFormat = config_get_string(activeConfiguration, "Video", "ColorFormat"); + const char *colorSpace = config_get_string(activeConfiguration, "Video", "ColorSpace"); + const char *colorRange = config_get_string(activeConfiguration, "Video", "ColorRange"); + + ovi.graphics_module = App()->GetRenderModule(); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, "Video", "OutputCY"); + ovi.output_format = GetVideoFormatFromName(colorFormat); + ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); + ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; + ovi.adapter = config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); + ovi.gpu_conversion = true; + ovi.scale_type = GetScaleType(activeConfiguration); + + if (ovi.base_width < 32 || ovi.base_height < 32) { + ovi.base_width = 1920; + ovi.base_height = 1080; + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); + } + + if (ovi.output_width < 32 || ovi.output_height < 32) { + ovi.output_width = ovi.base_width; + ovi.output_height = ovi.base_height; + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); + } + + ret = AttemptToResetVideo(&ovi); + if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) { + blog(LOG_WARNING, "Tried to reset when already active"); + return ret; + } + + if (ret == OBS_VIDEO_SUCCESS) { + ResizePreview(ovi.base_width, ovi.base_height); + if (program) + ResizeProgram(ovi.base_width, ovi.base_height); + + const float sdr_white_level = (float)config_get_uint(activeConfiguration, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = + (float)config_get_uint(activeConfiguration, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); + OBSBasicStats::InitializeValues(); + OBSProjector::UpdateMultiviewProjectors(); + + bool canMigrate = usingAbsoluteCoordinates || + (migrationBaseResolution && (migrationBaseResolution->first != ovi.base_width || + migrationBaseResolution->second != ovi.base_height)); + ui->actionRemigrateSceneCollection->setEnabled(canMigrate); + + emit CanvasResized(ovi.base_width, ovi.base_height); + emit OutputResized(ovi.output_width, ovi.output_height); + } + + return ret; +} + +bool OBSBasic::ResetAudio() +{ + ProfileScope("OBSBasic::ResetAudio"); + + struct obs_audio_info2 ai = {}; + ai.samples_per_sec = config_get_uint(activeConfiguration, "Audio", "SampleRate"); + + const char *channelSetupStr = config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + + if (strcmp(channelSetupStr, "Mono") == 0) + ai.speakers = SPEAKERS_MONO; + else if (strcmp(channelSetupStr, "2.1") == 0) + ai.speakers = SPEAKERS_2POINT1; + else if (strcmp(channelSetupStr, "4.0") == 0) + ai.speakers = SPEAKERS_4POINT0; + else if (strcmp(channelSetupStr, "4.1") == 0) + ai.speakers = SPEAKERS_4POINT1; + else if (strcmp(channelSetupStr, "5.1") == 0) + ai.speakers = SPEAKERS_5POINT1; + else if (strcmp(channelSetupStr, "7.1") == 0) + ai.speakers = SPEAKERS_7POINT1; + else + ai.speakers = SPEAKERS_STEREO; + + bool lowLatencyAudioBuffering = config_get_bool(App()->GetUserConfig(), "Audio", "LowLatencyAudioBuffering"); + if (lowLatencyAudioBuffering) { + ai.max_buffering_ms = 20; + ai.fixed_buffering = true; + } + + return obs_reset_audio2(&ai); +} + +extern char *get_new_source_name(const char *name, const char *format); + +void OBSBasic::ResetAudioDevice(const char *sourceId, const char *deviceId, const char *deviceDesc, int channel) +{ + bool disable = deviceId && strcmp(deviceId, "disabled") == 0; + OBSSourceAutoRelease source; + OBSDataAutoRelease settings; + + source = obs_get_output_source(channel); + if (source) { + if (disable) { + obs_set_output_source(channel, nullptr); + } else { + settings = obs_source_get_settings(source); + const char *oldId = obs_data_get_string(settings, "device_id"); + if (strcmp(oldId, deviceId) != 0) { + obs_data_set_string(settings, "device_id", deviceId); + obs_source_update(source, settings); + } + } + + } else if (!disable) { + BPtr name = get_new_source_name(deviceDesc, "%s (%d)"); + + settings = obs_data_create(); + obs_data_set_string(settings, "device_id", deviceId); + source = obs_source_create(sourceId, name, settings, nullptr); + + obs_set_output_source(channel, source); + } +} + +void OBSBasic::ResizePreview(uint32_t cx, uint32_t cy) +{ + QSize targetSize; + bool isFixedScaling; + obs_video_info ovi; + + /* resize preview panel to fix to the top section of the window */ + targetSize = GetPixelSize(ui->preview); + + isFixedScaling = ui->preview->IsFixedScaling(); + obs_get_video_info(&ovi); + + if (isFixedScaling) { + previewScale = ui->preview->GetScalingAmount(); + + ui->preview->ClampScrollingOffsets(); + + GetCenterPosFromFixedScale(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, + previewScale); + previewX += ui->preview->GetScrollX(); + previewY += ui->preview->GetScrollY(); + + } else { + GetScaleAndCenterPos(int(cx), int(cy), targetSize.width() - PREVIEW_EDGE_SIZE * 2, + targetSize.height() - PREVIEW_EDGE_SIZE * 2, previewX, previewY, previewScale); + } + + ui->preview->SetScalingAmount(previewScale); + + previewX += float(PREVIEW_EDGE_SIZE); + previewY += float(PREVIEW_EDGE_SIZE); +} + +void OBSBasic::CloseDialogs() +{ + QList childDialogs = this->findChildren(); + if (!childDialogs.isEmpty()) { + for (int i = 0; i < childDialogs.size(); ++i) { + childDialogs.at(i)->close(); + } + } + + if (!stats.isNull()) + stats->close(); //call close to save Stats geometry + if (!remux.isNull()) + remux->close(); +} + +void OBSBasic::EnumDialogs() +{ + visDialogs.clear(); + modalDialogs.clear(); + visMsgBoxes.clear(); + + /* fill list of Visible dialogs and Modal dialogs */ + QList dialogs = findChildren(); + for (QDialog *dialog : dialogs) { + if (dialog->isVisible()) + visDialogs.append(dialog); + if (dialog->isModal()) + modalDialogs.append(dialog); + } + + /* fill list of Visible message boxes */ + QList msgBoxes = findChildren(); + for (QMessageBox *msgbox : msgBoxes) { + if (msgbox->isVisible()) + visMsgBoxes.append(msgbox); + } +} + +void OBSBasic::ClearProjectors() +{ + for (size_t i = 0; i < projectors.size(); i++) { + if (projectors[i]) + delete projectors[i]; + } + + projectors.clear(); +} + +void OBSBasic::ClearSceneData() +{ + disableSaving++; + + setCursor(Qt::WaitCursor); + + CloseDialogs(); + + ClearVolumeControls(); + ClearListItems(ui->scenes); + ui->sources->Clear(); + ClearQuickTransitions(); + ui->transitions->clear(); + + ClearProjectors(); + + for (int i = 0; i < MAX_CHANNELS; i++) + obs_set_output_source(i, nullptr); + + /* Reset VCam to default to clear its private scene and any references + * it holds. It will be reconfigured during loading. */ + if (vcamEnabled) { + vcamConfig.type = VCamOutputType::ProgramView; + outputHandler->UpdateVirtualCamOutputSource(); + } + + collectionModuleData = nullptr; + lastScene = nullptr; + swapScene = nullptr; + programScene = nullptr; + prevFTBSource = nullptr; + + clipboard.clear(); + copyFiltersSource = nullptr; + copyFilter = nullptr; + + auto cb = [](void *, obs_source_t *source) { + obs_source_remove(source); + return true; + }; + + obs_enum_scenes(cb, nullptr); + obs_enum_sources(cb, nullptr); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP); + + undo_s.clear(); + + /* using QEvent::DeferredDelete explicitly is the only way to ensure + * that deleteLater events are processed at this point */ + QApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + do { + QApplication::sendPostedEvents(nullptr); + } while (obs_wait_for_destroy_queue()); + + /* Pump Qt events one final time to give remaining signals time to be + * processed (since this happens after the destroy thread finishes and + * the audio/video threads have processed their tasks). */ + QApplication::sendPostedEvents(nullptr); + + unsetCursor(); + + /* If scene data wasn't actually cleared, e.g. faulty plugin holding a + * reference, they will still be in the hash table, enumerate them and + * store the names for logging purposes. */ + auto cb2 = [](void *param, obs_source_t *source) { + auto orphans = static_cast *>(param); + orphans->push_back(obs_source_get_name(source)); + return true; + }; + + vector orphan_sources; + obs_enum_sources(cb2, &orphan_sources); + + if (!orphan_sources.empty()) { + /* Avoid logging list twice in case it gets called after + * setting the flag the first time. */ + if (!clearingFailed) { + /* This ugly mess exists to join a vector of strings + * with a user-defined delimiter. */ + string orphan_names = + std::accumulate(orphan_sources.begin(), orphan_sources.end(), string(""), + [](string a, string b) { return std::move(a) + "\n- " + b; }); + + blog(LOG_ERROR, "Not all sources were cleared when clearing scene data:\n%s\n", + orphan_names.c_str()); + } + + /* We do not decrement disableSaving here to avoid OBS + * overwriting user data with garbage. */ + clearingFailed = true; + } else { + disableSaving--; + + blog(LOG_INFO, "All scene data cleared"); + blog(LOG_INFO, "------------------------------------------------"); + } +} + +void OBSBasic::closeEvent(QCloseEvent *event) +{ + /* Wait for multitrack video stream to start/finish processing in the background */ + if (setupStreamingGuard.valid() && + setupStreamingGuard.wait_for(std::chrono::seconds{0}) != std::future_status::ready) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + + /* Do not close window if inside of a temporary event loop because we + * could be inside of an Auth::LoadUI call. Keep trying once per + * second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } + +#ifdef YOUTUBE_ENABLED + /* Also don't close the window if the youtube stream check is active */ + if (youtubeStreamCheckThread) { + QTimer::singleShot(1000, this, &OBSBasic::close); + event->ignore(); + return; + } +#endif + + if (isVisible()) + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + bool confirmOnExit = config_get_bool(App()->GetUserConfig(), "General", "ConfirmOnExit"); + + if (confirmOnExit && outputHandler && outputHandler->Active() && !clearingFailed) { + SetShowing(true); + + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmExit.Title"), QTStr("ConfirmExit.Text")); + + if (button == QMessageBox::No) { + event->ignore(); + restart = false; + return; + } + } + + if (remux && !remux->close()) { + event->ignore(); + restart = false; + return; + } + + QWidget::closeEvent(event); + if (!event->isAccepted()) + return; + + blog(LOG_INFO, SHUTDOWN_SEPARATOR); + + closing = true; + + /* While closing, a resize event to OBSQTDisplay could be triggered. + * The graphics thread on macOS dispatches a lambda function to be + * executed asynchronously in the main thread. However, the display is + * sometimes deleted before the lambda function is actually executed. + * To avoid such a case, destroy displays earlier than others such as + * deleting browser docks. */ + ui->preview->DestroyDisplay(); + if (program) + program->DestroyDisplay(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + if (introCheckThread) + introCheckThread->wait(); + if (whatsNewInitThread) + whatsNewInitThread->wait(); + if (updateCheckThread) + updateCheckThread->wait(); + if (logUploadThread) + logUploadThread->wait(); + if (devicePropertiesThread && devicePropertiesThread->isRunning()) { + devicePropertiesThread->wait(); + devicePropertiesThread.reset(); + } + + QApplication::sendPostedEvents(nullptr); + + signalHandlers.clear(); + + Auth::Save(); + SaveProjectNow(); + auth.reset(); + + delete extraBrowsers; + + config_set_string(App()->GetUserConfig(), "BasicWindow", "DockState", saveState().toBase64().constData()); + +#ifdef BROWSER_AVAILABLE + if (cef) + SaveExtraBrowserDocks(); + + ClearExtraBrowserDocks(); +#endif + + OnEvent(OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN); + + disableSaving++; + + /* Clear all scene data (dialogs, widgets, widget sub-items, scenes, + * sources, etc) so that all references are released before shutdown */ + ClearSceneData(); + + OnEvent(OBS_FRONTEND_EVENT_EXIT); + + // Destroys the frontend API so plugins can't continue calling it + obs_frontend_set_callbacks_internal(nullptr); + api = nullptr; + + QMetaObject::invokeMethod(App(), "quit", Qt::QueuedConnection); +} + +bool OBSBasic::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_MOVE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnMove(); + } + break; + case WM_DISPLAYCHANGE: + for (OBSQTDisplay *const display : findChildren()) { + display->OnDisplayChange(); + } + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSBasic::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *)event; + + if (isMinimized()) { + if (trayIcon && trayIcon->isVisible() && sysTrayMinimizeToTray()) { + ToggleShowHide(); + return; + } + + if (previewEnabled) + EnablePreviewDisplay(false); + } else if (stateEvent->oldState() & Qt::WindowMinimized && isVisible()) { + if (previewEnabled) + EnablePreviewDisplay(true); + } + } +} + +void OBSBasic::on_actionShow_Recordings_triggered() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = config_get_string(activeConfiguration, "AdvOut", "RecType"); + const char *adv_path = strcmp(type, "Standard") + ? config_get_string(activeConfiguration, "AdvOut", "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : adv_path; + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void OBSBasic::on_actionRemux_triggered() +{ + if (!remux.isNull()) { + remux->show(); + remux->raise(); + return; + } + + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); + + OBSRemux *remuxDlg; + remuxDlg = new OBSRemux(path, this); + remuxDlg->show(); + remux = remuxDlg; +} + +void OBSBasic::on_action_Settings_triggered() +{ + static bool settings_already_executing = false; + + /* Do not load settings window if inside of a temporary event loop + * because we could be inside of an Auth::LoadUI call. Keep trying + * once per second until we've exit any known sub-loops. */ + if (os_atomic_load_long(&insideEventLoop) != 0) { + QTimer::singleShot(1000, this, &OBSBasic::on_action_Settings_triggered); + return; + } + + if (settings_already_executing) { + return; + } + + settings_already_executing = true; + + { + OBSBasicSettings settings(this); + settings.exec(); + } + + settings_already_executing = false; + + if (restart) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("Restart"), QTStr("NeedsRestart")); + + if (button == QMessageBox::Yes) + close(); + else + restart = false; + } +} + +void OBSBasic::on_actionShowMacPermissions_triggered() +{ +#ifdef __APPLE__ + OBSPermissions check(this, CheckPermission(kScreenCapture), CheckPermission(kVideoDeviceAccess), + CheckPermission(kAudioDeviceAccess), CheckPermission(kAccessibility)); + check.exec(); +#endif +} + +void OBSBasic::ShowMissingFilesDialog(obs_missing_files_t *files) +{ + if (obs_missing_files_count(files) > 0) { + /* When loading the missing files dialog on launch, the + * window hasn't fully initialized by this point on macOS, + * so put this at the end of the current task queue. Fixes + * a bug where the window is behind OBS on startup. */ + QTimer::singleShot(0, [this, files] { + missDialog = new OBSMissingFiles(files, this); + missDialog->setAttribute(Qt::WA_DeleteOnClose, true); + missDialog->show(); + missDialog->raise(); + }); + } else { + obs_missing_files_destroy(files); + + /* Only raise dialog if triggered manually */ + if (!disableSaving) + OBSMessageBox::information(this, QTStr("MissingFiles.NoMissing.Title"), + QTStr("MissingFiles.NoMissing.Text")); + } +} + +void OBSBasic::on_actionShowMissingFiles_triggered() +{ + obs_missing_files_t *files = obs_missing_files_create(); + + auto cb_sources = [](void *data, obs_source_t *source) { + AddMissingFiles(data, source); + return true; + }; + + obs_enum_all_sources(cb_sources, files); + ShowMissingFilesDialog(files); +} + +void OBSBasic::on_actionAdvAudioProperties_triggered() +{ + if (advAudioWindow != nullptr) { + advAudioWindow->raise(); + return; + } + + bool iconsVisible = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons"); + + advAudioWindow = new OBSBasicAdvAudio(this); + advAudioWindow->show(); + advAudioWindow->setAttribute(Qt::WA_DeleteOnClose, true); + advAudioWindow->SetIconsVisible(iconsVisible); +} + +void OBSBasic::on_actionMixerToolbarAdvAudio_triggered() +{ + on_actionAdvAudioProperties_triggered(); +} + +void OBSBasic::on_actionMixerToolbarMenu_triggered() +{ + QAction unhideAllAction(QTStr("UnhideAll"), this); + connect(&unhideAllAction, &QAction::triggered, this, &OBSBasic::UnhideAllAudioControls, Qt::DirectConnection); + + QAction toggleControlLayoutAction(QTStr("VerticalLayout"), this); + toggleControlLayoutAction.setCheckable(true); + toggleControlLayoutAction.setChecked( + config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); + connect(&toggleControlLayoutAction, &QAction::changed, this, &OBSBasic::ToggleVolControlLayout, + Qt::DirectConnection); + + QMenu popup; + popup.addAction(&unhideAllAction); + popup.addSeparator(); + popup.addAction(&toggleControlLayoutAction); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) +{ + OBSSource source; + + if (current) { + OBSScene scene = GetOBSRef(current); + source = obs_scene_get_source(scene); + + currentScene = scene; + } else { + currentScene = NULL; + } + + SetCurrentScene(source); + + if (vcamEnabled && vcamConfig.type == VCamOutputType::PreviewOutput) + outputHandler->UpdateVirtualCamOutputSource(); + + OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); + + UpdateContextBar(); +} + +void OBSBasic::EditSceneName() +{ + ui->scenesDock->removeAction(renameScene); + QListWidgetItem *item = ui->scenes->currentItem(); + Qt::ItemFlags flags = item->flags(); + + item->setFlags(flags | Qt::ItemIsEditable); + ui->scenes->editItem(item); + item->setFlags(flags); +} + +QList OBSBasic::GetProjectorMenuMonitorsFormatted() +{ + QList projectorsFormatted; + 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(__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(QTStr("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())); + projectorsFormatted.push_back(str); + } + return projectorsFormatted; +} + +void OBSBasic::on_scenes_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = ui->scenes->itemAt(pos); + + QMenu popup(this); + QMenu order(QTStr("Basic.MainMenu.Edit.Order"), this); + + popup.addAction(QTStr("Add"), this, &OBSBasic::on_actionAddScene_triggered); + + if (item) { + QAction *copyFilters = new QAction(QTStr("Copy.Filters"), this); + copyFilters->setEnabled(false); + connect(copyFilters, &QAction::triggered, this, &OBSBasic::SceneCopyFilters); + QAction *pasteFilters = new QAction(QTStr("Paste.Filters"), this); + pasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource)); + connect(pasteFilters, &QAction::triggered, this, &OBSBasic::ScenePasteFilters); + + popup.addSeparator(); + popup.addAction(QTStr("Duplicate"), this, &OBSBasic::DuplicateSelectedScene); + popup.addAction(copyFilters); + popup.addAction(pasteFilters); + popup.addSeparator(); + popup.addAction(renameScene); + popup.addAction(ui->actionRemoveScene); + popup.addSeparator(); + + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveUp"), this, &OBSBasic::on_actionSceneUp_triggered); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveDown"), this, + &OBSBasic::on_actionSceneDown_triggered); + order.addSeparator(); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToTop"), this, &OBSBasic::MoveSceneToTop); + order.addAction(QTStr("Basic.MainMenu.Edit.Order.MoveToBottom"), this, &OBSBasic::MoveSceneToBottom); + popup.addMenu(&order); + + popup.addSeparator(); + + delete sceneProjectorMenu; + sceneProjectorMenu = new QMenu(QTStr("SceneProjector")); + AddProjectorMenuMonitors(sceneProjectorMenu, this, &OBSBasic::OpenSceneProjector); + popup.addMenu(sceneProjectorMenu); + + QAction *sceneWindow = popup.addAction(QTStr("SceneWindow"), this, &OBSBasic::OpenSceneWindow); + + popup.addAction(sceneWindow); + popup.addAction(QTStr("Screenshot.Scene"), this, &OBSBasic::ScreenshotScene); + popup.addSeparator(); + popup.addAction(QTStr("Filters"), this, &OBSBasic::OpenSceneFilters); + + popup.addSeparator(); + + delete perSceneTransitionMenu; + perSceneTransitionMenu = CreatePerSceneTransitionMenu(); + popup.addMenu(perSceneTransitionMenu); + + /* ---------------------- */ + + QAction *multiviewAction = popup.addAction(QTStr("ShowInMultiview")); + + OBSSource source = GetCurrentSceneSource(); + OBSDataAutoRelease data = obs_source_get_private_settings(source); + + obs_data_set_default_bool(data, "show_in_multiview", true); + bool show = obs_data_get_bool(data, "show_in_multiview"); + + multiviewAction->setCheckable(true); + multiviewAction->setChecked(show); + + auto showInMultiview = [](OBSData data) { + bool show = obs_data_get_bool(data, "show_in_multiview"); + obs_data_set_bool(data, "show_in_multiview", !show); + OBSProjector::UpdateMultiviewProjectors(); + }; + + connect(multiviewAction, &QAction::triggered, std::bind(showInMultiview, data.Get())); + + copyFilters->setEnabled(obs_source_filter_count(source) > 0); + } + + popup.addSeparator(); + + bool grid = ui->scenes->GetGridMode(); + + QAction *gridAction = new QAction(grid ? QTStr("Basic.Main.ListMode") : QTStr("Basic.Main.GridMode"), this); + connect(gridAction, &QAction::triggered, this, &OBSBasic::GridActionClicked); + popup.addAction(gridAction); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionSceneListMode_triggered() +{ + ui->scenes->SetGridMode(false); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_actionSceneGridMode_triggered() +{ + ui->scenes->SetGridMode(true); + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", true); +} + +void OBSBasic::GridActionClicked() +{ + bool gridMode = !ui->scenes->GetGridMode(); + ui->scenes->SetGridMode(gridMode); + + if (gridMode) + ui->actionSceneGridMode->setChecked(true); + else + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", gridMode); +} + +void OBSBasic::on_actionAddScene_triggered() +{ + string name; + QString format{QTStr("Basic.Main.DefaultSceneName.Text")}; + + int i = 2; + QString placeHolderText = format.arg(i); + OBSSourceAutoRelease source = nullptr; + while ((source = obs_get_source_by_name(QT_TO_UTF8(placeHolderText)))) { + placeHolderText = format.arg(++i); + } + + bool accepted = NameDialog::AskForName(this, QTStr("Basic.Main.AddSceneDlg.Title"), + QTStr("Basic.Main.AddSceneDlg.Text"), name, placeHolderText); + + if (accepted) { + if (name.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + on_actionAddScene_triggered(); + return; + } + + OBSSourceAutoRelease source = obs_get_source_by_name(name.c_str()); + if (source) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + + on_actionAddScene_triggered(); + return; + } + + auto undo_fn = [](const std::string &data) { + obs_source_t *t = obs_get_source_by_name(data.c_str()); + if (t) { + obs_source_remove(t); + obs_source_release(t); + } + }; + + auto redo_fn = [this](const std::string &data) { + OBSSceneAutoRelease scene = obs_scene_create(data.c_str()); + obs_source_t *source = obs_scene_get_source(scene); + SetCurrentScene(source, true); + }; + undo_s.add_action(QTStr("Undo.Add").arg(QString(name.c_str())), undo_fn, redo_fn, name, name); + + OBSSceneAutoRelease scene = obs_scene_create(name.c_str()); + obs_source_t *scene_source = obs_scene_get_source(scene); + SetCurrentScene(scene_source); + } +} + +void OBSBasic::on_actionRemoveScene_triggered() +{ + RemoveSelectedScene(); +} + +void OBSBasic::ChangeSceneIndex(bool relative, int offset, int invalidIdx) +{ + int idx = ui->scenes->currentRow(); + if (idx == -1 || idx == invalidIdx) + return; + + ui->scenes->blockSignals(true); + QListWidgetItem *item = ui->scenes->takeItem(idx); + + if (!relative) + idx = 0; + + ui->scenes->insertItem(idx + offset, item); + ui->scenes->setCurrentRow(idx + offset); + item->setSelected(true); + currentScene = GetOBSRef(item).Get(); + ui->scenes->blockSignals(false); + + OBSProjector::UpdateMultiviewProjectors(); +} + +void OBSBasic::on_actionSceneUp_triggered() +{ + ChangeSceneIndex(true, -1, 0); +} + +void OBSBasic::on_actionSceneDown_triggered() +{ + ChangeSceneIndex(true, 1, ui->scenes->count() - 1); +} + +void OBSBasic::MoveSceneToTop() +{ + ChangeSceneIndex(false, 0, 0); +} + +void OBSBasic::MoveSceneToBottom() +{ + ChangeSceneIndex(false, ui->scenes->count() - 1, ui->scenes->count() - 1); +} + +void OBSBasic::EditSceneItemName() +{ + int idx = GetTopSelectedSourceItem(); + ui->sources->Edit(idx); +} + +void OBSBasic::SetDeinterlacingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_mode mode = (obs_deinterlace_mode)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_mode(source, mode); +} + +void OBSBasic::SetDeinterlacingOrder() +{ + QAction *action = reinterpret_cast(sender()); + obs_deinterlace_field_order order = (obs_deinterlace_field_order)action->property("order").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + + obs_source_set_deinterlace_field_order(source, order); +} + +QMenu *OBSBasic::AddDeinterlacingMenu(QMenu *menu, obs_source_t *source) +{ + obs_deinterlace_mode deinterlaceMode = obs_source_get_deinterlace_mode(source); + obs_deinterlace_field_order deinterlaceOrder = obs_source_get_deinterlace_field_order(source); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetDeinterlacingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceMode == mode); + + ADD_MODE("Disable", OBS_DEINTERLACE_MODE_DISABLE); + ADD_MODE("Deinterlacing.Discard", OBS_DEINTERLACE_MODE_DISCARD); + ADD_MODE("Deinterlacing.Retro", OBS_DEINTERLACE_MODE_RETRO); + ADD_MODE("Deinterlacing.Blend", OBS_DEINTERLACE_MODE_BLEND); + ADD_MODE("Deinterlacing.Blend2x", OBS_DEINTERLACE_MODE_BLEND_2X); + ADD_MODE("Deinterlacing.Linear", OBS_DEINTERLACE_MODE_LINEAR); + ADD_MODE("Deinterlacing.Linear2x", OBS_DEINTERLACE_MODE_LINEAR_2X); + ADD_MODE("Deinterlacing.Yadif", OBS_DEINTERLACE_MODE_YADIF); + ADD_MODE("Deinterlacing.Yadif2x", OBS_DEINTERLACE_MODE_YADIF_2X); +#undef ADD_MODE + + menu->addSeparator(); + +#define ADD_ORDER(name, order) \ + action = menu->addAction(QTStr("Deinterlacing." name), this, &OBSBasic::SetDeinterlacingOrder); \ + action->setProperty("order", (int)order); \ + action->setCheckable(true); \ + action->setChecked(deinterlaceOrder == order); + + ADD_ORDER("TopFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_TOP); + ADD_ORDER("BottomFieldFirst", OBS_DEINTERLACE_FIELD_ORDER_BOTTOM); +#undef ADD_ORDER + + return menu; +} + +void OBSBasic::SetScaleFilter() +{ + QAction *action = reinterpret_cast(sender()); + obs_scale_type mode = (obs_scale_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_scale_filter(sceneItem, mode); +} + +QMenu *OBSBasic::AddScaleFilteringMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_scale_type scaleFilter = obs_sceneitem_get_scale_filter(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetScaleFilter); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(scaleFilter == mode); + + ADD_MODE("Disable", OBS_SCALE_DISABLE); + ADD_MODE("ScaleFiltering.Point", OBS_SCALE_POINT); + ADD_MODE("ScaleFiltering.Bilinear", OBS_SCALE_BILINEAR); + ADD_MODE("ScaleFiltering.Bicubic", OBS_SCALE_BICUBIC); + ADD_MODE("ScaleFiltering.Lanczos", OBS_SCALE_LANCZOS); + ADD_MODE("ScaleFiltering.Area", OBS_SCALE_AREA); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMethod() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_method method = (obs_blending_method)action->property("method").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_method(sceneItem, method); +} + +QMenu *OBSBasic::AddBlendingMethodMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_method blendingMethod = obs_sceneitem_get_blending_method(item); + QAction *action; + +#define ADD_MODE(name, method) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMethod); \ + action->setProperty("method", (int)method); \ + action->setCheckable(true); \ + action->setChecked(blendingMethod == method); + + ADD_MODE("BlendingMethod.Default", OBS_BLEND_METHOD_DEFAULT); + ADD_MODE("BlendingMethod.SrgbOff", OBS_BLEND_METHOD_SRGB_OFF); +#undef ADD_MODE + + return menu; +} + +void OBSBasic::SetBlendingMode() +{ + QAction *action = reinterpret_cast(sender()); + obs_blending_type mode = (obs_blending_type)action->property("mode").toInt(); + OBSSceneItem sceneItem = GetCurrentSceneItem(); + + obs_sceneitem_set_blending_mode(sceneItem, mode); +} + +QMenu *OBSBasic::AddBlendingModeMenu(QMenu *menu, obs_sceneitem_t *item) +{ + obs_blending_type blendingMode = obs_sceneitem_get_blending_mode(item); + QAction *action; + +#define ADD_MODE(name, mode) \ + action = menu->addAction(QTStr("" name), this, &OBSBasic::SetBlendingMode); \ + action->setProperty("mode", (int)mode); \ + action->setCheckable(true); \ + action->setChecked(blendingMode == mode); + + ADD_MODE("BlendingMode.Normal", OBS_BLEND_NORMAL); + ADD_MODE("BlendingMode.Additive", OBS_BLEND_ADDITIVE); + ADD_MODE("BlendingMode.Subtract", OBS_BLEND_SUBTRACT); + ADD_MODE("BlendingMode.Screen", OBS_BLEND_SCREEN); + ADD_MODE("BlendingMode.Multiply", OBS_BLEND_MULTIPLY); + ADD_MODE("BlendingMode.Lighten", OBS_BLEND_LIGHTEN); + ADD_MODE("BlendingMode.Darken", OBS_BLEND_DARKEN); +#undef ADD_MODE + + return menu; +} + +QMenu *OBSBasic::AddBackgroundColorMenu(QMenu *menu, QWidgetAction *widgetAction, ColorSelect *select, + obs_sceneitem_t *item) +{ + QAction *action; + + menu->setStyleSheet(QString("*[bgColor=\"1\"]{background-color:rgba(255,68,68,33%);}" + "*[bgColor=\"2\"]{background-color:rgba(255,255,68,33%);}" + "*[bgColor=\"3\"]{background-color:rgba(68,255,68,33%);}" + "*[bgColor=\"4\"]{background-color:rgba(68,255,255,33%);}" + "*[bgColor=\"5\"]{background-color:rgba(68,68,255,33%);}" + "*[bgColor=\"6\"]{background-color:rgba(255,68,255,33%);}" + "*[bgColor=\"7\"]{background-color:rgba(68,68,68,33%);}" + "*[bgColor=\"8\"]{background-color:rgba(255,255,255,33%);}")); + + obs_data_t *privData = obs_sceneitem_get_private_settings(item); + obs_data_release(privData); + + obs_data_set_default_int(privData, "color-preset", 0); + int preset = obs_data_get_int(privData, "color-preset"); + + action = menu->addAction(QTStr("Clear"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 0); + action->setChecked(preset == 0); + + action = menu->addAction(QTStr("CustomColor"), this, &OBSBasic::ColorChange); + action->setCheckable(true); + action->setProperty("bgColor", 1); + action->setChecked(preset == 1); + + menu->addSeparator(); + + widgetAction->setDefaultWidget(select); + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *colorButton = select->findChild(button.str().c_str()); + if (preset == i + 1) + colorButton->setStyleSheet("border: 2px solid black"); + + colorButton->setProperty("bgColor", i); + select->connect(colorButton, &QPushButton::released, this, &OBSBasic::ColorChange); + } + + menu->addAction(widgetAction); + + return menu; +} + +ColorSelect::ColorSelect(QWidget *parent) : QWidget(parent), ui(new Ui::ColorSelect) +{ + ui->setupUi(this); +} + +void OBSBasic::CreateSourcePopupMenu(int idx, bool preview) +{ + QMenu popup(this); + delete previewProjectorSource; + delete sourceProjector; + delete scaleFilteringMenu; + delete blendingMethodMenu; + delete blendingModeMenu; + delete colorMenu; + delete colorWidgetAction; + delete colorSelect; + delete deinterlaceMenu; + + if (preview) { + QAction *action = + popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + if (IsPreviewProgramMode()) + action->setEnabled(false); + + popup.addAction(ui->actionLockPreview); + popup.addMenu(ui->scalingMenu); + + previewProjectorSource = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorSource, this, &OBSBasic::OpenPreviewProjector); + + popup.addMenu(previewProjectorSource); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addAction(previewWindow); + + popup.addAction(QTStr("Screenshot.Preview"), this, &OBSBasic::ScreenshotScene); + + popup.addSeparator(); + } + + QPointer addSourceMenu = CreateAddSourcePopupMenu(); + if (addSourceMenu) + popup.addMenu(addSourceMenu); + + if (ui->sources->MultipleBaseSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.GroupItems"), ui->sources, &SourceTree::GroupSelectedItems); + + } else if (ui->sources->GroupsSelected()) { + popup.addSeparator(); + popup.addAction(QTStr("Basic.Main.Ungroup"), ui->sources, &SourceTree::UngroupSelectedGroups); + } + + popup.addSeparator(); + popup.addAction(ui->actionCopySource); + popup.addAction(ui->actionPasteRef); + popup.addAction(ui->actionPasteDup); + popup.addSeparator(); + + popup.addSeparator(); + popup.addAction(ui->actionCopyFilters); + popup.addAction(ui->actionPasteFilters); + popup.addSeparator(); + + if (idx != -1) { + if (addSourceMenu) + popup.addSeparator(); + + OBSSceneItem sceneItem = ui->sources->Get(idx); + obs_source_t *source = obs_sceneitem_get_source(sceneItem); + uint32_t flags = obs_source_get_output_flags(source); + bool isAsyncVideo = (flags & OBS_SOURCE_ASYNC_VIDEO) == OBS_SOURCE_ASYNC_VIDEO; + bool hasAudio = (flags & OBS_SOURCE_AUDIO) == OBS_SOURCE_AUDIO; + bool hasVideo = (flags & OBS_SOURCE_VIDEO) == OBS_SOURCE_VIDEO; + + colorMenu = new QMenu(QTStr("ChangeBG")); + colorWidgetAction = new QWidgetAction(colorMenu); + colorSelect = new ColorSelect(colorMenu); + popup.addMenu(AddBackgroundColorMenu(colorMenu, colorWidgetAction, colorSelect, sceneItem)); + popup.addAction(renameSource); + popup.addAction(ui->actionRemoveSource); + popup.addSeparator(); + + popup.addMenu(ui->orderMenu); + + if (hasVideo) + popup.addMenu(ui->transformMenu); + + popup.addSeparator(); + + if (hasAudio) { + QAction *actionHideMixer = + popup.addAction(QTStr("HideMixer"), this, &OBSBasic::ToggleHideMixer); + actionHideMixer->setCheckable(true); + actionHideMixer->setChecked(SourceMixerHidden(source)); + popup.addSeparator(); + } + + if (hasVideo) { + QAction *resizeOutput = popup.addAction(QTStr("ResizeOutputSizeOfSource"), this, + &OBSBasic::ResizeOutputSizeOfSource); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + resizeOutput->setEnabled(!obs_video_active()); + + if (width < 32 || height < 32) + resizeOutput->setEnabled(false); + + scaleFilteringMenu = new QMenu(QTStr("ScaleFiltering")); + popup.addMenu(AddScaleFilteringMenu(scaleFilteringMenu, sceneItem)); + blendingModeMenu = new QMenu(QTStr("BlendingMode")); + popup.addMenu(AddBlendingModeMenu(blendingModeMenu, sceneItem)); + blendingMethodMenu = new QMenu(QTStr("BlendingMethod")); + popup.addMenu(AddBlendingMethodMenu(blendingMethodMenu, sceneItem)); + if (isAsyncVideo) { + deinterlaceMenu = new QMenu(QTStr("Deinterlacing")); + popup.addMenu(AddDeinterlacingMenu(deinterlaceMenu, source)); + } + + popup.addSeparator(); + + popup.addMenu(CreateVisibilityTransitionMenu(true)); + popup.addMenu(CreateVisibilityTransitionMenu(false)); + popup.addSeparator(); + + sourceProjector = new QMenu(QTStr("SourceProjector")); + AddProjectorMenuMonitors(sourceProjector, this, &OBSBasic::OpenSourceProjector); + popup.addMenu(sourceProjector); + popup.addAction(QTStr("SourceWindow"), this, &OBSBasic::OpenSourceWindow); + + popup.addAction(QTStr("Screenshot.Source"), this, &OBSBasic::ScreenshotSelectedSource); + } + + popup.addSeparator(); + + if (flags & OBS_SOURCE_INTERACTION) + popup.addAction(QTStr("Interact"), this, &OBSBasic::on_actionInteract_triggered); + + popup.addAction(QTStr("Filters"), this, [&]() { OpenFilters(); }); + QAction *action = + popup.addAction(QTStr("Properties"), this, &OBSBasic::on_actionSourceProperties_triggered); + action->setEnabled(obs_source_configurable(source)); + } + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_sources_customContextMenuRequested(const QPoint &pos) +{ + if (ui->scenes->count()) { + QModelIndex idx = ui->sources->indexAt(pos); + CreateSourcePopupMenu(idx.row(), false); + } +} + +void OBSBasic::on_scenes_itemDoubleClicked(QListWidgetItem *witem) +{ + if (!witem) + return; + + if (IsPreviewProgramMode()) { + bool doubleClickSwitch = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "TransitionOnDoubleClick"); + + if (doubleClickSwitch) + TransitionClicked(); + } +} + +static inline bool should_show_properties(obs_source_t *source, const char *id) +{ + if (!source) + return false; + if (strcmp(id, "group") == 0) + return false; + if (!obs_source_configurable(source)) + return false; + + uint32_t caps = obs_source_get_output_flags(source); + if ((caps & OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES) != 0) + return false; + + return true; +} + +void OBSBasic::AddSource(const char *id) +{ + if (id && *id) { + OBSBasicSourceSelect sourceSelect(this, id, undo_s); + sourceSelect.exec(); + if (should_show_properties(sourceSelect.newSource, id)) { + CreatePropertiesWindow(sourceSelect.newSource); + } + } +} + +QMenu *OBSBasic::CreateAddSourcePopupMenu() +{ + const char *unversioned_type; + const char *type; + bool foundValues = false; + bool foundDeprecated = false; + size_t idx = 0; + + QMenu *popup = new QMenu(QTStr("Add"), this); + QMenu *deprecated = new QMenu(QTStr("Deprecated"), popup); + + auto getActionAfter = [](QMenu *menu, const QString &name) { + QList actions = menu->actions(); + + for (QAction *menuAction : actions) { + if (menuAction->text().compare(name, Qt::CaseInsensitive) >= 0) + return menuAction; + } + + return (QAction *)nullptr; + }; + + auto addSource = [this, getActionAfter](QMenu *popup, const char *type, const char *name) { + QString qname = QT_UTF8(name); + QAction *popupItem = new QAction(qname, this); + connect(popupItem, &QAction::triggered, [this, type]() { AddSource(type); }); + + QIcon icon; + + if (strcmp(type, "scene") == 0) + icon = GetSceneIcon(); + else + icon = GetSourceIcon(type); + + popupItem->setIcon(icon); + + QAction *after = getActionAfter(popup, qname); + popup->insertAction(after, popupItem); + }; + + while (obs_enum_input_types2(idx++, &type, &unversioned_type)) { + const char *name = obs_source_get_display_name(type); + uint32_t caps = obs_get_source_output_flags(type); + + if ((caps & OBS_SOURCE_CAP_DISABLED) != 0) + continue; + + if ((caps & OBS_SOURCE_DEPRECATED) == 0) { + addSource(popup, unversioned_type, name); + } else { + addSource(deprecated, unversioned_type, name); + foundDeprecated = true; + } + foundValues = true; + } + + addSource(popup, "scene", Str("Basic.Scene")); + + popup->addSeparator(); + QAction *addGroup = new QAction(QTStr("Group"), this); + addGroup->setIcon(GetGroupIcon()); + connect(addGroup, &QAction::triggered, [this]() { AddSource("group"); }); + popup->addAction(addGroup); + + if (!foundDeprecated) { + delete deprecated; + deprecated = nullptr; + } + + if (!foundValues) { + delete popup; + popup = nullptr; + + } else if (foundDeprecated) { + popup->addSeparator(); + popup->addMenu(deprecated); + } + + return popup; +} + +void OBSBasic::AddSourcePopupMenu(const QPoint &pos) +{ + if (!GetCurrentScene()) { + // Tell the user he needs a scene first (help beginners). + OBSMessageBox::information(this, QTStr("Basic.Main.AddSourceHelp.Title"), + QTStr("Basic.Main.AddSourceHelp.Text")); + return; + } + + QScopedPointer popup(CreateAddSourcePopupMenu()); + if (popup) + popup->exec(pos); +} + +void OBSBasic::on_actionAddSource_triggered() +{ + AddSourcePopupMenu(QCursor::pos()); +} + +static bool remove_items(obs_scene_t *, obs_sceneitem_t *item, void *param) +{ + vector &items = *reinterpret_cast *>(param); + + if (obs_sceneitem_selected(item)) { + items.emplace_back(item); + } else if (obs_sceneitem_is_group(item)) { + obs_sceneitem_group_enum_items(item, remove_items, &items); + } + return true; +}; + +OBSData OBSBasic::BackupScene(obs_scene_t *scene, std::vector *sources) +{ + OBSDataArrayAutoRelease undo_array = obs_data_array_create(); + + if (!sources) { + obs_scene_enum_items(scene, save_undo_source_enum, undo_array); + } else { + for (obs_source_t *source : *sources) { + obs_data_t *source_data = obs_save_source(source); + obs_data_array_push_back(undo_array, source_data); + obs_data_release(source_data); + } + } + + OBSDataAutoRelease scene_data = obs_save_source(obs_scene_get_source(scene)); + obs_data_array_push_back(undo_array, scene_data); + + OBSDataAutoRelease data = obs_data_create(); + + obs_data_set_array(data, "array", undo_array); + obs_data_get_json(data); + return data.Get(); +} + +static bool add_source_enum(obs_scene_t *, obs_sceneitem_t *item, void *p) +{ + auto sources = static_cast *>(p); + sources->push_back(obs_sceneitem_get_source(item)); + return true; +} + +void OBSBasic::CreateSceneUndoRedoAction(const QString &action_name, OBSData undo_data, OBSData redo_data) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease base = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(base, "array"); + std::vector sources; + std::vector old_sources; + + /* create missing sources */ + const size_t count = obs_data_array_count(array); + sources.reserve(count); + + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease data = obs_data_array_item(array, i); + const char *name = obs_data_get_string(data, "name"); + + OBSSourceAutoRelease source = obs_get_source_by_name(name); + if (!source) + source = obs_load_source(data); + + sources.push_back(source.Get()); + + /* update scene/group settings to restore their + * contents to their saved settings */ + obs_scene_t *scene = obs_group_or_scene_from_source(source); + if (scene) { + obs_scene_enum_items(scene, add_source_enum, &old_sources); + OBSDataAutoRelease scene_settings = obs_data_get_obj(data, "settings"); + obs_source_update(source, scene_settings); + } + } + + /* actually load sources now */ + for (obs_source_t *source : sources) + obs_source_load2(source); + + ui->sources->RefreshItems(); + }; + + const char *undo_json = obs_data_get_last_json(undo_data); + const char *redo_json = obs_data_get_last_json(redo_data); + + undo_s.add_action(action_name, undo_redo, undo_redo, undo_json, redo_json); +} + +void OBSBasic::on_actionRemoveSource_triggered() +{ + vector items; + OBSScene scene = GetCurrentScene(); + obs_source_t *scene_source = obs_scene_get_source(scene); + + obs_scene_enum_items(scene, remove_items, &items); + + if (!items.size()) + return; + + /* ------------------------------------- */ + /* confirm action with user */ + + bool confirmed = false; + + if (items.size() > 1) { + QString text = QTStr("ConfirmRemove.TextMultiple").arg(QString::number(items.size())); + + QMessageBox remove_items(this); + remove_items.setText(text); + QPushButton *Yes = remove_items.addButton(QTStr("Yes"), QMessageBox::YesRole); + remove_items.setDefaultButton(Yes); + remove_items.addButton(QTStr("No"), QMessageBox::NoRole); + remove_items.setIcon(QMessageBox::Question); + remove_items.setWindowTitle(QTStr("ConfirmRemove.Title")); + remove_items.exec(); + + confirmed = Yes == remove_items.clickedButton(); + } else { + OBSSceneItem &item = items[0]; + obs_source_t *source = obs_sceneitem_get_source(item); + if (source && QueryRemoveSource(source)) + confirmed = true; + } + if (!confirmed) + return; + + /* ----------------------------------------------- */ + /* save undo data */ + + OBSData undo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* remove items */ + + for (auto &item : items) + obs_sceneitem_remove(item); + + /* ----------------------------------------------- */ + /* save redo data */ + + OBSData redo_data = BackupScene(scene_source); + + /* ----------------------------------------------- */ + /* add undo/redo action */ + + QString action_name; + if (items.size() > 1) { + action_name = QTStr("Undo.Sources.Multi").arg(QString::number(items.size())); + } else { + QString str = QTStr("Undo.Delete"); + action_name = str.arg(obs_source_get_name(obs_sceneitem_get_source(items[0]))); + } + + CreateSceneUndoRedoAction(action_name, undo_data, redo_data); +} + +void OBSBasic::on_actionInteract_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreateInteractionWindow(source); +} + +void OBSBasic::on_actionSourceProperties_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + OBSSource source = obs_sceneitem_get_source(item); + + if (source) + CreatePropertiesWindow(source); +} + +void OBSBasic::MoveSceneItem(enum obs_order_movement movement, const QString &action_name) +{ + OBSSceneItem item = GetCurrentSceneItem(); + obs_source_t *source = obs_sceneitem_get_source(item); + + if (!source) + return; + + OBSScene scene = GetCurrentScene(); + std::vector sources; + if (scene != obs_sceneitem_get_scene(item)) + sources.push_back(obs_scene_get_source(obs_sceneitem_get_scene(item))); + + OBSData undo_data = BackupScene(scene, &sources); + + obs_sceneitem_set_order(item, movement); + + const char *source_name = obs_source_get_name(source); + const char *scene_name = obs_source_get_name(obs_scene_get_source(scene)); + + OBSData redo_data = BackupScene(scene, &sources); + CreateSceneUndoRedoAction(action_name.arg(source_name, scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionSourceUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionSourceDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveUp_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_UP, QTStr("Undo.MoveUp")); +} + +void OBSBasic::on_actionMoveDown_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_DOWN, QTStr("Undo.MoveDown")); +} + +void OBSBasic::on_actionMoveToTop_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_TOP, QTStr("Undo.MoveToTop")); +} + +void OBSBasic::on_actionMoveToBottom_triggered() +{ + MoveSceneItem(OBS_ORDER_MOVE_BOTTOM, QTStr("Undo.MoveToBottom")); +} + +static BPtr ReadLogFile(const char *subdir, const char *log) +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), subdir) <= 0) + return nullptr; + + string path = logDir; + path += "/"; + path += log; + + BPtr file = os_quick_read_utf8_file(path.c_str()); + if (!file) + blog(LOG_WARNING, "Failed to read log file %s", path.c_str()); + + return file; +} + +void OBSBasic::UploadLog(const char *subdir, const char *file, const bool crash) +{ + BPtr fileString{ReadLogFile(subdir, file)}; + + if (!fileString) + return; + + if (!*fileString) + return; + + ui->menuLogFiles->setEnabled(false); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(false); +#endif + + stringstream ss; + ss << "OBS " << App()->GetVersionString(false) << " log file uploaded at " << CurrentDateTimeString() << "\n\n" + << fileString; + + if (logUploadThread) { + logUploadThread->wait(); + } + + RemoteTextThread *thread = new RemoteTextThread("https://obsproject.com/logs/upload", "text/plain", ss.str()); + + logUploadThread.reset(thread); + if (crash) { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::crashUploadFinished); + } else { + connect(thread, &RemoteTextThread::Result, this, &OBSBasic::logUploadFinished); + } + logUploadThread->start(); +} + +void OBSBasic::on_actionShowLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/logs") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadCurrentLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetCurrentLog(), false); +} + +void OBSBasic::on_actionUploadLastLog_triggered() +{ + UploadLog("obs-studio/logs", App()->GetLastLog(), false); +} + +void OBSBasic::on_actionViewCurrentLog_triggered() +{ + if (!logView) + logView = new OBSLogViewer(); + + logView->show(); + logView->setWindowState((logView->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); + logView->activateWindow(); + logView->raise(); +} + +void OBSBasic::on_actionShowCrashLogs_triggered() +{ + char logDir[512]; + if (GetAppConfigPath(logDir, sizeof(logDir), "obs-studio/crashes") <= 0) + return; + + QUrl url = QUrl::fromLocalFile(QT_UTF8(logDir)); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionUploadLastCrashLog_triggered() +{ + UploadLog("obs-studio/crashes", App()->GetLastCrashLog(), true); +} + +void OBSBasic::on_actionCheckForUpdates_triggered() +{ + CheckForUpdates(true); +} + +void OBSBasic::on_actionRepair_triggered() +{ +#if defined(_WIN32) + ui->actionCheckForUpdates->setEnabled(false); + ui->actionRepair->setEnabled(false); + + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + updateCheckThread.reset(new AutoUpdateThread(false, true)); + updateCheckThread->start(); +#endif +} + +void OBSBasic::on_actionRestartSafe_triggered() +{ + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), safe_mode ? QTStr("SafeMode.RestartNormal") : QTStr("SafeMode.Restart")); + + if (button == QMessageBox::Yes) { + restart = safe_mode; + restart_safe = !safe_mode; + close(); + } +} + +void OBSBasic::logUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, false); +} + +void OBSBasic::crashUploadFinished(const QString &text, const QString &error) +{ + ui->menuLogFiles->setEnabled(true); +#if defined(_WIN32) + ui->menuCrashLogs->setEnabled(true); +#endif + + if (text.isEmpty()) { + OBSMessageBox::critical(this, QTStr("LogReturnDialog.ErrorUploadingLog"), error); + return; + } + openLogDialog(text, true); +} + +void OBSBasic::openLogDialog(const QString &text, const bool crash) +{ + + OBSDataAutoRelease returnData = obs_data_create_from_json(QT_TO_UTF8(text)); + string resURL = obs_data_get_string(returnData, "url"); + QString logURL = resURL.c_str(); + + OBSLogReply logDialog(this, logURL, crash); + logDialog.exec(); +} + +static void RenameListItem(OBSBasic *parent, QListWidget *listWidget, obs_source_t *source, const string &name) +{ + const char *prevName = obs_source_get_name(source); + if (name == prevName) + return; + + OBSSourceAutoRelease foundSource = obs_get_source_by_name(name.c_str()); + QListWidgetItem *listItem = listWidget->currentItem(); + + if (foundSource || name.empty()) { + listItem->setText(QT_UTF8(prevName)); + + if (foundSource) { + OBSMessageBox::warning(parent, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + } else if (name.empty()) { + OBSMessageBox::warning(parent, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + } + } else { + auto undo = [prev = std::string(prevName)](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, prev.c_str()); + }; + + auto redo = [name](const std::string &data) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(data.c_str()); + obs_source_set_name(source, name.c_str()); + }; + + std::string source_uuid(obs_source_get_uuid(source)); + parent->undo_s.add_action(QTStr("Undo.Rename").arg(name.c_str()), undo, redo, source_uuid, source_uuid); + + listItem->setText(QT_UTF8(name.c_str())); + obs_source_set_name(source, name.c_str()); + } +} + +void OBSBasic::SceneNameEdited(QWidget *editor) +{ + OBSScene scene = GetCurrentScene(); + QLineEdit *edit = qobject_cast(editor); + string text = QT_TO_UTF8(edit->text().trimmed()); + + if (!scene) + return; + + obs_source_t *source = obs_scene_get_source(scene); + RenameListItem(this, ui->scenes, source, text); + + ui->scenesDock->addAction(renameScene); + + OnEvent(OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED); +} + +void OBSBasic::OpenFilters(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateFiltersWindow(source); +} + +void OBSBasic::OpenProperties(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreatePropertiesWindow(source); +} + +void OBSBasic::OpenInteraction(OBSSource source) +{ + if (source == nullptr) { + OBSSceneItem item = GetCurrentSceneItem(); + source = obs_sceneitem_get_source(item); + } + CreateInteractionWindow(source); +} + +void OBSBasic::OpenEditTransform(OBSSceneItem item) +{ + if (!item) + item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::OpenSceneFilters() +{ + OBSScene scene = GetCurrentScene(); + OBSSource source = obs_scene_get_source(scene); + + CreateFiltersWindow(source); +} + +#define RECORDING_START "==== Recording Start ===============================================" +#define RECORDING_STOP "==== Recording Stop ================================================" +#define REPLAY_BUFFER_START "==== Replay Buffer Start ===========================================" +#define REPLAY_BUFFER_STOP "==== Replay Buffer Stop ============================================" +#define STREAMING_START "==== Streaming Start ===============================================" +#define STREAMING_STOP "==== Streaming Stop ================================================" +#define VIRTUAL_CAM_START "==== Virtual Camera Start ==========================================" +#define VIRTUAL_CAM_STOP "==== Virtual Camera Stop ===========================================" + +void OBSBasic::DisplayStreamStartError() +{ + QString message = !outputHandler->lastError.empty() ? QTStr(outputHandler->lastError.c_str()) + : QTStr("Output.StartFailedGeneric"); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); +} + +#ifdef YOUTUBE_ENABLED +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, const QString &stream_id, const QString &key, + bool autostart, bool autostop, bool start_now) +{ + //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); + + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); + + obs_service_update(service_obj, settings); + autoStartBroadcast = autostart; + autoStopBroadcast = autostop; + broadcastReady = true; + + emit BroadcastStreamReady(broadcastReady); + + if (start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} + +void OBSBasic::YoutubeStreamCheck(const std::string &key) +{ + YoutubeApiWrappers *apiYouTube(dynamic_cast(GetAuth())); + if (!apiYouTube) { + /* technically we should never get here -Lain */ + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + youtubeStreamCheckThread->deleteLater(); + blog(LOG_ERROR, "=========================================="); + blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); + blog(LOG_ERROR, "=========================================="); + return; + } + + int timeout = 0; + json11::Json json; + QString id = key.c_str(); + + while (StreamingActive()) { + if (timeout == 14) { + QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); + break; + } + + if (!apiYouTube->FindStream(id, json)) { + QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); + break; + } + + auto item = json["items"][0]; + auto status = item["status"]["streamStatus"].string_value(); + if (status == "active") { + emit BroadcastStreamActive(); + break; + } else { + QThread::sleep(1); + timeout++; + } + } + + youtubeStreamCheckThread->deleteLater(); +} + +void OBSBasic::ShowYouTubeAutoStartWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("YouTube.Actions.AutoStartStreamingWarning.Title")); + msgbox.setText(QTStr("YouTube.Actions.AutoStartStreamingWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} +#endif + +void OBSBasic::StartStreaming() +{ + if (outputHandler->StreamingActive()) + return; + if (disableOutputsRef) + return; + + if (auth && auth->broadcastFlow()) { + if (!broadcastActive && !broadcastReady) { + QMessageBox no_broadcast(this); + no_broadcast.setText(QTStr("Output.NoBroadcast.Text")); + QPushButton *SetupBroadcast = + no_broadcast.addButton(QTStr("Basic.Main.SetupBroadcast"), QMessageBox::YesRole); + no_broadcast.setDefaultButton(SetupBroadcast); + no_broadcast.addButton(QTStr("Close"), QMessageBox::NoRole); + no_broadcast.setIcon(QMessageBox::Information); + no_broadcast.setWindowTitle(QTStr("Output.NoBroadcast.Title")); + no_broadcast.exec(); + + if (no_broadcast.clickedButton() == SetupBroadcast) + QMetaObject::invokeMethod(this, "SetupBroadcast"); + return; + } + } + + emit StreamingPreparing(); + + if (sysTrayStream) { + sysTrayStream->setEnabled(false); + sysTrayStream->setText("Basic.Main.PreparingStream"); + } + + auto finish_stream_setup = [&](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTING); + + SaveProject(); + + emit StreamingStarting(autoStartBroadcast); + + if (sysTrayStream) + sysTrayStream->setText("Basic.Main.Connecting"); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (autoStartBroadcast) { + emit BroadcastStreamStarted(autoStopBroadcast); + broadcastActive = true; + } + + bool recordWhenStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); +#endif + }; + + setupStreamingGuard = outputHandler->SetupStreaming(service, finish_stream_setup); +} + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); +} + +void OBSBasic::SetupBroadcast() +{ +#ifdef YOUTUBE_ENABLED + Auth *const auth = GetAuth(); + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +} + +#ifdef _WIN32 +static inline void UpdateProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority(priority); +} + +static inline void ClearProcessPriority() +{ + const char *priority = config_get_string(App()->GetAppConfig(), "General", "ProcessPriority"); + if (priority && strcmp(priority, "Normal") != 0) + SetProcessPriority("Normal"); +} +#else +#define UpdateProcessPriority() \ + do { \ + } while (false) +#define ClearProcessPriority() \ + do { \ + } while (false) +#endif + +inline void OBSBasic::OnActivate(bool force) +{ + if (ui->profileMenu->isEnabled() || force) { + ui->profileMenu->setEnabled(false); + ui->autoConfigure->setEnabled(false); + App()->IncrementSleepInhibition(); + UpdateProcessPriority(); + + struct obs_video_info ovi; + obs_get_video_info(&ovi); + lastOutputResolution = {ovi.base_width, ovi.base_height}; + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayMask = QIcon(":/res/images/tray_active_macos.svg"); + trayMask.setIsMask(true); + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayMask)); +#else + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", QIcon(":/res/images/tray_active.png"))); +#endif + } + } +} + +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +inline void OBSBasic::OnDeactivate() +{ + if (!outputHandler->Active() && !ui->profileMenu->isEnabled()) { + ui->profileMenu->setEnabled(true); + ui->autoConfigure->setEnabled(true); + App()->DecrementSleepInhibition(); + ClearProcessPriority(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusInactive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray", trayIconFile)); + } + } else if (outputHandler->Active() && trayIcon && trayIcon->isVisible()) { + if (os_atomic_load_bool(&recording_paused)) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + } else { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + } + } +} + +void OBSBasic::StopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(streamingStopping); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::ForceStopStreaming() +{ + SaveProject(); + + if (outputHandler->StreamingActive()) + outputHandler->StopStreaming(true); + + // special case: force reset broadcast state if + // no autostart and no autostop selected + if (!autoStartBroadcast && !broadcastActive) { + broadcastActive = false; + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = false; + } + + if (autoStopBroadcast) { + broadcastActive = false; + broadcastReady = false; + } + + emit BroadcastStreamReady(broadcastReady); + + OnDeactivate(); + + bool recordWhenStreaming = config_get_bool(App()->GetUserConfig(), "BasicWindow", "RecordWhenStreaming"); + bool keepRecordingWhenStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepRecordingWhenStreamStops"); + if (recordWhenStreaming && !keepRecordingWhenStreamStops) + StopRecording(); + + bool replayBufferWhileStreaming = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); + bool keepReplayBufferStreamStops = + config_get_bool(App()->GetUserConfig(), "BasicWindow", "KeepReplayBufferStreamStops"); + if (replayBufferWhileStreaming && !keepReplayBufferStreamStops) + StopReplayBuffer(); +} + +void OBSBasic::StreamDelayStarting(int sec) +{ + emit StreamingStarted(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStarting(sec); + + OnActivate(); +} + +void OBSBasic::StreamDelayStopping(int sec) +{ + emit StreamingStopped(true); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + ui->statusbar->StreamDelayStopping(sec); + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStart() +{ + emit StreamingStarted(); + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->statusbar->StreamStarted(output); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StopStreaming")); + sysTrayStream->setEnabled(true); + } + +#ifdef YOUTUBE_ENABLED + if (!autoStartBroadcast) { + // get a current stream key + obs_service_t *service_obj = GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + std::string key = obs_data_get_string(settings, "stream_id"); + if (!key.empty() && !youtubeStreamCheckThread) { + youtubeStreamCheckThread = CreateQThread([this, key] { YoutubeStreamCheck(key); }); + youtubeStreamCheckThread->setObjectName("YouTubeStreamCheckThread"); + youtubeStreamCheckThread->start(); + } + } +#endif + + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STARTED); + + OnActivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + + blog(LOG_INFO, STREAMING_START); +} + +void OBSBasic::StreamStopping() +{ + emit StreamingStopping(); + + if (sysTrayStream) + sysTrayStream->setText(QTStr("Basic.Main.StoppingStreaming")); + + streamingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPING); +} + +void OBSBasic::StreamingStop(int code, QString last_error) +{ + const char *errorDescription = ""; + DStr errorMessage; + bool use_last_error = false; + bool encode_error = false; + + switch (code) { + case OBS_OUTPUT_BAD_PATH: + errorDescription = Str("Output.ConnectFail.BadPath"); + break; + + case OBS_OUTPUT_CONNECT_FAILED: + use_last_error = true; + errorDescription = Str("Output.ConnectFail.ConnectFailed"); + break; + + case OBS_OUTPUT_INVALID_STREAM: + errorDescription = Str("Output.ConnectFail.InvalidStream"); + break; + + case OBS_OUTPUT_ENCODE_ERROR: + encode_error = true; + break; + + case OBS_OUTPUT_HDR_DISABLED: + errorDescription = Str("Output.ConnectFail.HdrDisabled"); + break; + + default: + case OBS_OUTPUT_ERROR: + use_last_error = true; + errorDescription = Str("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 = Str("Output.ConnectFail.Disconnected"); + } + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s\n\n%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + ui->statusbar->StreamStopped(); + + emit StreamingStopped(); + + if (sysTrayStream) { + sysTrayStream->setText(QTStr("Basic.Main.StartStreaming")); + sysTrayStream->setEnabled(true); + } + + streamingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_STREAMING_STOPPED); + + OnDeactivate(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + + blog(LOG_INFO, STREAMING_STOP); + + if (encode_error) { + QString msg = last_error.isEmpty() ? QTStr("Output.StreamEncodeError.Msg") + : QTStr("Output.StreamEncodeError.Msg.LastError").arg(last_error); + OBSMessageBox::information(this, QTStr("Output.StreamEncodeError.Title"), msg); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::information(this, QTStr("Output.ConnectFail.Title"), QT_UTF8(errorMessage)); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QT_UTF8(errorDescription), QSystemTrayIcon::Warning); + } + + // Reset broadcast button state/text + if (!broadcastActive) + SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); +} + +void OBSBasic::AutoRemux(QString input, bool no_show) +{ + auto config = Config(); + + bool autoRemux = config_get_bool(config, "Video", "AutoRemux"); + + if (!autoRemux) + return; + + bool isSimpleMode = false; + + const char *mode = config_get_string(config, "Output", "Mode"); + if (!mode) { + isSimpleMode = true; + } else { + isSimpleMode = strcmp(mode, "Simple") == 0; + } + + if (!isSimpleMode) { + const char *recType = config_get_string(config, "AdvOut", "RecType"); + + bool ffmpegOutput = astrcmpi(recType, "FFmpeg") == 0; + + if (ffmpegOutput) + return; + } + + if (input.isEmpty()) + return; + + QFileInfo fi(input); + QString suffix = fi.suffix(); + + /* do not remux if lossless */ + if (suffix.compare("avi", Qt::CaseInsensitive) == 0) { + return; + } + + QString path = fi.path(); + + QString output = input; + output.resize(output.size() - suffix.size()); + + const obs_encoder_t *videoEncoder = obs_output_get_video_encoder(outputHandler->fileOutput); + const char *vCodecName = obs_encoder_get_codec(videoEncoder); + const char *format = config_get_string(config, isSimpleMode ? "SimpleOutput" : "AdvOut", "RecFormat2"); + + /* Retain original container for fMP4/fMOV */ + if (strncmp(format, "fragmented", 10) == 0) { + output += "remuxed." + suffix; + } else if (strcmp(vCodecName, "prores") == 0) { + output += "mov"; + } else { + output += "mp4"; + } + + OBSRemux *remux = new OBSRemux(QT_TO_UTF8(path), this, true); + if (!no_show) + remux->show(); + remux->AutoRemux(input, output); +} + +void OBSBasic::StartRecording() +{ + if (outputHandler->RecordingActive()) + return; + if (disableOutputsRef) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (!IsFFmpegOutputToURL() && LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTING); + + SaveProject(); + + outputHandler->StartRecording(); +} + +void OBSBasic::RecordStopping() +{ + emit RecordingStopping(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StoppingRecording")); + + recordingStopping = true; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPING); +} + +void OBSBasic::StopRecording() +{ + SaveProject(); + + if (outputHandler->RecordingActive()) + outputHandler->StopRecording(recordingStopping); + + OnDeactivate(); +} + +void OBSBasic::RecordingStart() +{ + ui->statusbar->RecordingStarted(outputHandler->fileOutput); + emit RecordingStarted(isRecordingPausable); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StopRecording")); + + recordingStopping = false; + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STARTED); + + if (!diskFullTimer->isActive()) + diskFullTimer->start(1000); + + OnActivate(); + + blog(LOG_INFO, RECORDING_START); +} + +void OBSBasic::RecordingStop(int code, QString last_error) +{ + ui->statusbar->RecordingStopped(); + emit RecordingStopped(); + + if (sysTrayRecord) + sysTrayRecord->setText(QTStr("Basic.Main.StartRecording")); + + blog(LOG_INFO, RECORDING_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_ENCODE_ERROR && isVisible()) { + QString msg = last_error.isEmpty() + ? QTStr("Output.RecordError.EncodeErrorMsg") + : QTStr("Output.RecordError.EncodeErrorMsg.LastError").arg(last_error); + OBSMessageBox::warning(this, QTStr("Output.RecordError.Title"), msg); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + + const char *errorDescription; + DStr errorMessage; + bool use_last_error = true; + + errorDescription = Str("Output.RecordError.Msg"); + + if (use_last_error && !last_error.isEmpty()) + dstr_printf(errorMessage, "%s

%s", errorDescription, QT_TO_UTF8(last_error)); + else + dstr_copy(errorMessage, errorDescription); + + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QT_UTF8(errorMessage)); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } else if (code == OBS_OUTPUT_SUCCESS) { + if (outputHandler) { + std::string path = outputHandler->lastRecordingPath; + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(QT_UTF8(path.c_str()))); + } + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_STOPPED); + + if (diskFullTimer->isActive()) + diskFullTimer->stop(); + + AutoRemux(outputHandler->lastRecordingPath.c_str()); + + OnDeactivate(); +} + +void OBSBasic::RecordingFileChanged(QString lastRecordingPath) +{ + QString str = QTStr("Basic.StatusBar.RecordingSavedTo"); + ShowStatusBarMessage(str.arg(lastRecordingPath)); + + AutoRemux(lastRecordingPath, true); +} + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); + } +} + +void OBSBasic::StartReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (outputHandler->ReplayBufferActive()) + return; + if (disableOutputsRef) + return; + + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + if (!OutputPathValid()) { + OutputPathInvalidMessage(); + return; + } + + if (LowDiskSpace()) { + DiskSpaceMessage(); + return; + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); + + SaveProject(); + + if (outputHandler->StartReplayBuffer() && os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::ReplayBufferStopping() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopping(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StoppingReplayBuffer")); + + replayBufferStopping = true; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING); +} + +void OBSBasic::StopReplayBuffer() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + SaveProject(); + + if (outputHandler->ReplayBufferActive()) + outputHandler->StopReplayBuffer(replayBufferStopping); + + OnDeactivate(); +} + +void OBSBasic::ReplayBufferStart() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStarted(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StopReplayBuffer")); + + replayBufferStopping = false; + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED); + + OnActivate(); + + blog(LOG_INFO, REPLAY_BUFFER_START); +} + +void OBSBasic::ReplayBufferSave() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "save", &cd); + calldata_free(&cd); +} + +void OBSBasic::ReplayBufferSaved() +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + if (!outputHandler->ReplayBufferActive()) + return; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(outputHandler->replayBuffer); + proc_handler_call(ph, "get_last_replay", &cd); + std::string path = calldata_string(&cd, "path"); + QString msg = QTStr("Basic.StatusBar.ReplayBufferSavedTo").arg(QT_UTF8(path.c_str())); + ShowStatusBarMessage(msg); + lastReplay = path; + calldata_free(&cd); + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED); + + AutoRemux(QT_UTF8(path.c_str())); +} + +void OBSBasic::ReplayBufferStop(int code) +{ + if (!outputHandler || !outputHandler->replayBuffer) + return; + + emit ReplayBufStopped(); + + if (sysTrayReplayBuffer) + sysTrayReplayBuffer->setText(QTStr("Basic.Main.StartReplayBuffer")); + + blog(LOG_INFO, REPLAY_BUFFER_STOP); + + if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordFail.Title"), QTStr("Output.RecordFail.Unsupported")); + + } else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) { + OBSMessageBox::warning(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); + + } else if (code != OBS_OUTPUT_SUCCESS && isVisible()) { + OBSMessageBox::critical(this, QTStr("Output.RecordError.Title"), QTStr("Output.RecordError.Msg")); + + } else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordFail.Unsupported"), QSystemTrayIcon::Warning); + + } else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"), QSystemTrayIcon::Warning); + + } else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) { + SysTrayNotify(QTStr("Output.RecordError.Msg"), QSystemTrayIcon::Warning); + } + + OnEvent(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED); + + OnDeactivate(); +} + +void OBSBasic::StartVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + if (outputHandler->VirtualCamActive()) + return; + if (disableOutputsRef) + return; + + SaveProject(); + + outputHandler->StartVirtualCam(); +} + +void OBSBasic::StopVirtualCam() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + SaveProject(); + + if (outputHandler->VirtualCamActive()) + outputHandler->StopVirtualCam(); + + OnDeactivate(); +} + +void OBSBasic::OnVirtualCamStart() +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStarted(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED); + + OnActivate(); + + blog(LOG_INFO, VIRTUAL_CAM_START); +} + +void OBSBasic::OnVirtualCamStop(int) +{ + if (!outputHandler || !outputHandler->virtualCam) + return; + + emit VirtualCamStopped(); + + if (sysTrayVirtualCam) + sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam")); + + OnEvent(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED); + + blog(LOG_INFO, VIRTUAL_CAM_STOP); + + OnDeactivate(); + + if (!restartingVCam) + return; + + /* Restarting needs to be delayed to make sure that the virtual camera + * implementation is stopped and avoid race condition. */ + QTimer::singleShot(100, this, &OBSBasic::RestartingVirtualCam); +} + +void OBSBasic::StreamActionTriggered() +{ + if (outputHandler->StreamingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + +#ifdef YOUTUBE_ENABLED + if (isVisible() && auth && IsYouTubeService(auth->service()) && autoStopBroadcast) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + + confirm = false; + } +#endif + if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStop.Title"), QTStr("ConfirmStop.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StopStreaming(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + Auth *auth = GetAuth(); + + auto action = (auth && auth->external()) ? StreamSettingsAction::ContinueStream + : UIValidation::StreamSettingsConfirmation(this, service); + switch (action) { + case StreamSettingsAction::ContinueStream: + break; + case StreamSettingsAction::OpenSettings: + on_action_Settings_triggered(); + return; + case StreamSettingsAction::Cancel: + return; + } + + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStartingStream"); + + bool bwtest = false; + + if (this->auth) { + OBSDataAutoRelease settings = obs_service_get_settings(service); + bwtest = obs_data_get_bool(settings, "bwtest"); + // Disable confirmation if this is going to open broadcast setup + if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) + confirm = false; + } + + if (bwtest && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question(this, QTStr("ConfirmBWTest.Title"), + QTStr("ConfirmBWTest.Text")); + + if (button == QMessageBox::No) + return; + } else if (confirm && isVisible()) { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ConfirmStart.Title"), QTStr("ConfirmStart.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + StartStreaming(); + } +} + +void OBSBasic::RecordActionTriggered() +{ + if (outputHandler->RecordingActive()) { + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingRecord"); + + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStopRecord.Title"), QTStr("ConfirmStopRecord.Text"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + StopRecording(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartRecording(); + } +} + +void OBSBasic::VirtualCamActionTriggered() +{ + if (outputHandler->VirtualCamActive()) { + StopVirtualCam(); + } else { + if (!UIValidation::NoSourcesConfirmation(this)) + return; + + StartVirtualCam(); + } +} + +void OBSBasic::OpenVirtualCamConfig() +{ + OBSBasicVCamConfig dialog(vcamConfig, outputHandler->VirtualCamActive(), this); + + connect(&dialog, &OBSBasicVCamConfig::Accepted, this, &OBSBasic::UpdateVirtualCamConfig); + connect(&dialog, &OBSBasicVCamConfig::AcceptedAndRestart, this, &OBSBasic::RestartVirtualCam); + + dialog.exec(); +} + +void log_vcam_changed(const VCamConfig &config, bool starting) +{ + const char *action = starting ? "Starting" : "Changing"; + + switch (config.type) { + case VCamOutputType::Invalid: + break; + case VCamOutputType::ProgramView: + blog(LOG_INFO, "%s Virtual Camera output to Program", action); + break; + case VCamOutputType::PreviewOutput: + blog(LOG_INFO, "%s Virtual Camera output to Preview", action); + break; + case VCamOutputType::SceneOutput: + blog(LOG_INFO, "%s Virtual Camera output to Scene : %s", action, config.scene.c_str()); + break; + case VCamOutputType::SourceOutput: + blog(LOG_INFO, "%s Virtual Camera output to Source : %s", action, config.source.c_str()); + break; + } +} + +void OBSBasic::UpdateVirtualCamConfig(const VCamConfig &config) +{ + vcamConfig = config; + + outputHandler->UpdateVirtualCamOutputSource(); + log_vcam_changed(config, false); +} + +void OBSBasic::RestartVirtualCam(const VCamConfig &config) +{ + restartingVCam = true; + + StopVirtualCam(); + + vcamConfig = config; +} + +void OBSBasic::RestartingVirtualCam() +{ + if (!restartingVCam) + return; + + outputHandler->UpdateVirtualCamOutputSource(); + StartVirtualCam(); + restartingVCam = false; +} + +void OBSBasic::on_actionHelpPortal_triggered() +{ + QUrl url = QUrl("https://obsproject.com/help", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionWebsite_triggered() +{ + QUrl url = QUrl("https://obsproject.com", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionDiscord_triggered() +{ + QUrl url = QUrl("https://obsproject.com/discord", QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowWhatsNew_triggered() +{ +#ifdef WHATSNEW_ENABLED + if (introCheckThread && introCheckThread->isRunning()) + return; + if (!cef) + return; + + config_set_int(App()->GetAppConfig(), "General", "InfoIncrement", -1); + + WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); + connect(wnit, &WhatsNewInfoThread::Result, this, &OBSBasic::ReceivedIntroJson, Qt::QueuedConnection); + + introCheckThread.reset(wnit); + introCheckThread->start(); +#endif +} + +void OBSBasic::on_actionReleaseNotes_triggered() +{ + QString addr("https://github.com/obsproject/obs-studio/releases"); + QUrl url(QString("%1/%2").arg(addr, obs_get_version_string()), QUrl::TolerantMode); + QDesktopServices::openUrl(url); +} + +void OBSBasic::on_actionShowSettingsFolder_triggered() +{ + const std::string userConfigPath = App()->userConfigLocation.u8string() + "/obs-studio"; + const QString userConfigLocation = QString::fromStdString(userConfigPath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userConfigLocation)); +} + +void OBSBasic::on_actionShowProfileFolder_triggered() +{ + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + QString currentProfileLocation = QString::fromStdString(currentProfile.path.u8string()); + + QDesktopServices::openUrl(QUrl::fromLocalFile(currentProfileLocation)); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +int OBSBasic::GetTopSelectedSourceItem() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + return selectedItems.count() ? selectedItems[0].row() : -1; +} + +QModelIndexList OBSBasic::GetAllSelectedSourceItems() +{ + return ui->sources->selectionModel()->selectedIndexes(); +} + +void OBSBasic::on_preview_customContextMenuRequested() +{ + CreateSourcePopupMenu(GetTopSelectedSourceItem(), true); +} + +void OBSBasic::ProgramViewContextMenuRequested() +{ + QMenu popup(this); + QPointer studioProgramProjector; + + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + + popup.addMenu(studioProgramProjector); + popup.addAction(QTStr("StudioProgramWindow"), this, &OBSBasic::OpenStudioProgramWindow); + popup.addAction(QTStr("Screenshot.StudioProgram"), this, &OBSBasic::ScreenshotProgram); + + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_previewDisabledWidget_customContextMenuRequested() +{ + QMenu popup(this); + delete previewProjectorMain; + + QAction *action = popup.addAction(QTStr("Basic.Main.PreviewConextMenu.Enable"), this, &OBSBasic::TogglePreview); + action->setCheckable(true); + action->setChecked(obs_display_enabled(ui->preview->GetDisplay())); + + previewProjectorMain = new QMenu(QTStr("PreviewProjector")); + AddProjectorMenuMonitors(previewProjectorMain, this, &OBSBasic::OpenPreviewProjector); + + QAction *previewWindow = popup.addAction(QTStr("PreviewWindow"), this, &OBSBasic::OpenPreviewWindow); + + popup.addMenu(previewProjectorMain); + popup.addAction(previewWindow); + popup.exec(QCursor::pos()); +} + +void OBSBasic::on_actionAlwaysOnTop_triggered() +{ +#ifndef _WIN32 + /* Make sure all dialogs are safely and successfully closed before + * switching the always on top mode due to the fact that windows all + * have to be recreated, so queue the actual toggle to happen after + * all events related to closing the dialogs have finished */ + CloseDialogs(); +#endif + + QMetaObject::invokeMethod(this, "ToggleAlwaysOnTop", Qt::QueuedConnection); +} + +void OBSBasic::ToggleAlwaysOnTop() +{ + bool isAlwaysOnTop = IsAlwaysOnTop(this); + + ui->actionAlwaysOnTop->setChecked(!isAlwaysOnTop); + SetAlwaysOnTop(this, !isAlwaysOnTop); + + show(); +} + +void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const +{ + const char *val = config_get_string(activeConfiguration, "Video", "FPSCommon"); + + if (strcmp(val, "10") == 0) { + num = 10; + den = 1; + } else if (strcmp(val, "20") == 0) { + num = 20; + den = 1; + } else if (strcmp(val, "24 NTSC") == 0) { + num = 24000; + den = 1001; + } else if (strcmp(val, "25 PAL") == 0) { + num = 25; + den = 1; + } else if (strcmp(val, "29.97") == 0) { + num = 30000; + den = 1001; + } else if (strcmp(val, "48") == 0) { + num = 48; + den = 1; + } else if (strcmp(val, "50 PAL") == 0) { + num = 50; + den = 1; + } else if (strcmp(val, "59.94") == 0) { + num = 60000; + den = 1001; + } else if (strcmp(val, "60") == 0) { + num = 60; + den = 1; + } else { + num = 30; + den = 1; + } +} + +void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); + den = 1; +} + +void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const +{ + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); +} + +void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const +{ + num = 1000000000; + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); +} + +void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const +{ + uint32_t type = config_get_uint(activeConfiguration, "Video", "FPSType"); + + if (type == 1) //"Integer" + GetFPSInteger(num, den); + else if (type == 2) //"Fraction" + GetFPSFraction(num, den); + /* + * else if (false) //"Nanoseconds", currently not implemented + * GetFPSNanoseconds(num, den); + */ + else + GetFPSCommon(num, den); +} + +config_t *OBSBasic::Config() const +{ + return activeConfiguration; +} + +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000 +#endif + +void OBSBasic::NewYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + /* make sure that the youtube app dock can't be immediately recreated. + * dumb hack. blame chromium. or this particular dock. or both. if CEF + * creates/destroys/creates a widget too quickly it can lead to a + * crash. */ + uint64_t ts = os_gettime_ns(); + if ((ts - lastYouTubeAppDockCreationTime) < (5ULL * SEC_TO_NSEC)) + return; + + lastYouTubeAppDockCreationTime = ts; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = new YouTubeAppDock("YouTube Live Control Panel"); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (!cef_js_avail) + return; + + if (youtubeAppDock) + RemoveDockWidget(youtubeAppDock->objectName()); + + youtubeAppDock = nullptr; +} +#endif + +void OBSBasic::UpdateEditMenu() +{ + QModelIndexList items = GetAllSelectedSourceItems(); + int totalCount = items.count(); + size_t filter_count = 0; + + if (totalCount == 1) { + OBSSceneItem sceneItem = ui->sources->Get(GetTopSelectedSourceItem()); + OBSSource source = obs_sceneitem_get_source(sceneItem); + filter_count = obs_source_filter_count(source); + } + + bool allowPastingDuplicate = !!clipboard.size(); + for (size_t i = clipboard.size(); i > 0; i--) { + const size_t idx = i - 1; + OBSWeakSource &weak = clipboard[idx].weak_source; + if (obs_weak_source_expired(weak)) { + clipboard.erase(clipboard.begin() + idx); + continue; + } + OBSSourceAutoRelease strong = obs_weak_source_get_source(weak.Get()); + if (allowPastingDuplicate && obs_source_get_output_flags(strong) & OBS_SOURCE_DO_NOT_DUPLICATE) + allowPastingDuplicate = false; + } + + int videoCount = 0; + bool canTransformMultiple = false; + for (int i = 0; i < totalCount; i++) { + OBSSceneItem item = ui->sources->Get(items.value(i).row()); + OBSSource source = obs_sceneitem_get_source(item); + const uint32_t flags = obs_source_get_output_flags(source); + const bool hasVideo = (flags & OBS_SOURCE_VIDEO) != 0; + if (hasVideo && !obs_sceneitem_locked(item)) + canTransformMultiple = true; + + if (hasVideo) + videoCount++; + } + const bool canTransformSingle = videoCount == 1 && totalCount == 1; + + OBSSceneItem curItem = GetCurrentSceneItem(); + bool locked = curItem && obs_sceneitem_locked(curItem); + + ui->actionCopySource->setEnabled(totalCount > 0); + ui->actionEditTransform->setEnabled(canTransformSingle && !locked); + ui->actionCopyTransform->setEnabled(canTransformSingle); + ui->actionPasteTransform->setEnabled(canTransformMultiple && hasCopiedTransform && videoCount > 0); + ui->actionCopyFilters->setEnabled(filter_count > 0); + ui->actionPasteFilters->setEnabled(!obs_weak_source_expired(copyFiltersSource) && totalCount > 0); + ui->actionPasteRef->setEnabled(!!clipboard.size()); + ui->actionPasteDup->setEnabled(allowPastingDuplicate); + + ui->actionMoveUp->setEnabled(totalCount > 0); + ui->actionMoveDown->setEnabled(totalCount > 0); + ui->actionMoveToTop->setEnabled(totalCount > 0); + ui->actionMoveToBottom->setEnabled(totalCount > 0); + + ui->actionResetTransform->setEnabled(canTransformMultiple); + ui->actionRotate90CW->setEnabled(canTransformMultiple); + ui->actionRotate90CCW->setEnabled(canTransformMultiple); + ui->actionRotate180->setEnabled(canTransformMultiple); + ui->actionFlipHorizontal->setEnabled(canTransformMultiple); + ui->actionFlipVertical->setEnabled(canTransformMultiple); + ui->actionFitToScreen->setEnabled(canTransformMultiple); + ui->actionStretchToScreen->setEnabled(canTransformMultiple); + ui->actionCenterToScreen->setEnabled(canTransformMultiple); + ui->actionVerticalCenter->setEnabled(canTransformMultiple); + ui->actionHorizontalCenter->setEnabled(canTransformMultiple); +} + +void OBSBasic::on_actionEditTransform_triggered() +{ + const auto item = GetCurrentSceneItem(); + if (!item) + return; + CreateEditTransformWindow(item); +} + +void OBSBasic::CreateEditTransformWindow(obs_sceneitem_t *item) +{ + if (transformWindow) + transformWindow->close(); + transformWindow = new OBSBasicTransform(item, this); + connect(ui->scenes, &QListWidget::currentItemChanged, transformWindow, &OBSBasicTransform::OnSceneChanged); + transformWindow->show(); + transformWindow->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::on_actionCopyTransform_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + obs_sceneitem_get_info2(item, &copiedTransformInfo); + obs_sceneitem_get_crop(item, &copiedCropInfo); + + ui->actionPasteTransform->setEnabled(true); + hasCopiedTransform = true; +} + +void undo_redo(const std::string &data) +{ + OBSDataAutoRelease dat = obs_data_create_from_json(data.c_str()); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(dat, "scene_uuid")); + reinterpret_cast(App()->GetMainWindow())->SetCurrentScene(source.Get(), true); + + obs_scene_load_transform_states(data.c_str()); +} + +void OBSBasic::on_actionPasteTransform_triggered() +{ + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + auto func = [](obs_scene_t *, obs_sceneitem_t *item, void *data) { + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + OBSBasic *main = reinterpret_cast(data); + + obs_sceneitem_defer_update_begin(item); + obs_sceneitem_set_info2(item, &main->copiedTransformInfo); + obs_sceneitem_set_crop(item, &main->copiedCropInfo); + obs_sceneitem_defer_update_end(item); + + return true; + }; + + obs_scene_enum_items(GetCurrentScene(), func, this); + + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Paste").arg(obs_source_get_name(GetCurrentSceneSource())), undo_redo, + undo_redo, undo_data, redo_data); +} + +static bool reset_tr(obs_scene_t * /* scene */, obs_sceneitem_t *item, void *) +{ + if (obs_sceneitem_is_group(item)) + obs_sceneitem_group_enum_items(item, reset_tr, nullptr); + if (!obs_sceneitem_selected(item)) + return true; + if (obs_sceneitem_locked(item)) + return true; + + obs_sceneitem_defer_update_begin(item); + + obs_transform_info info; + vec2_set(&info.pos, 0.0f, 0.0f); + vec2_set(&info.scale, 1.0f, 1.0f); + info.rot = 0.0f; + info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT; + info.bounds_type = OBS_BOUNDS_NONE; + info.bounds_alignment = OBS_ALIGN_CENTER; + info.crop_to_bounds = false; + vec2_set(&info.bounds, 0.0f, 0.0f); + obs_sceneitem_set_info2(item, &info); + + obs_sceneitem_crop crop = {}; + obs_sceneitem_set_crop(item, &crop); + + obs_sceneitem_defer_update_end(item); + + return true; +} + +void OBSBasic::on_actionResetTransform_triggered() +{ + OBSScene scene = GetCurrentScene(); + + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(scene, false); + obs_scene_enum_items(scene, reset_tr, nullptr); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(scene, false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.Reset").arg(obs_source_get_name(obs_scene_get_source(scene))), + undo_redo, undo_redo, undo_data, redo_data); + + obs_scene_enum_items(GetCurrentScene(), reset_tr, nullptr); +} + +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); + + return true; +}; + +void OBSBasic::on_actionRotate90CW_triggered() +{ + float f90CW = 90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate90CCW_triggered() +{ + float f90CCW = -90.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f90CCW); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionRotate180_triggered() +{ + float f180 = 180.0f; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), RotateSelectedSources, &f180); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Rotate").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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); + + return true; +} + +void OBSBasic::on_actionFlipHorizontal_triggered() +{ + vec2 scale; + vec2_set(&scale, -1.0f, 1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionFlipVertical_triggered() +{ + vec2 scale; + vec2_set(&scale, 1.0f, -1.0f); + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), MultiplySelectedItemScale, &scale); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VFlip").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +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_video_info ovi; + obs_get_video_info(&ovi); + + 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(ovi.base_width), float(ovi.base_height)); + 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); + + return true; +} + +void OBSBasic::on_actionFitToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_SCALE_INNER; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.FitToScreen").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionStretchToScreen_triggered() +{ + obs_bounds_type boundsType = OBS_BOUNDS_STRETCH; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + obs_scene_enum_items(GetCurrentScene(), CenterAlignSelectedItems, &boundsType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action(QTStr("Undo.Transform.StretchToScreen") + .arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::CenterSelectedSceneItems(const CenterType ¢erType) +{ + QModelIndexList selectedItems = GetAllSelectedSourceItems(); + + if (!selectedItems.count()) + return; + + vector items; + + // Filter out items that have no size + for (int x = 0; x < selectedItems.count(); x++) { + OBSSceneItem item = ui->sources->Get(selectedItems[x].row()); + obs_transform_info oti; + obs_sceneitem_get_info2(item, &oti); + + obs_source_t *source = obs_sceneitem_get_source(item); + float width = float(obs_source_get_width(source)) * oti.scale.x; + float height = float(obs_source_get_height(source)) * oti.scale.y; + + if (width == 0.0f || height == 0.0f) + continue; + + items.emplace_back(item); + } + + 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 + obs_video_info ovi; + obs_get_video_info(&ovi); + + vec3 screenCenter; + vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_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 OBSBasic::on_actionCenterToScreen_triggered() +{ + CenterType centerType = CenterType::Scene; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.Center").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionVerticalCenter_triggered() +{ + CenterType centerType = CenterType::Vertical; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.VCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::on_actionHorizontalCenter_triggered() +{ + CenterType centerType = CenterType::Horizontal; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + CenterSelectedSceneItems(centerType); + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), false); + + std::string undo_data(obs_data_get_json(wrapper)); + std::string redo_data(obs_data_get_json(rwrapper)); + undo_s.add_action( + QTStr("Undo.Transform.HCenter").arg(obs_source_get_name(obs_scene_get_source(GetCurrentScene()))), + undo_redo, undo_redo, undo_data, redo_data); +} + +void OBSBasic::EnablePreviewDisplay(bool enable) +{ + obs_display_set_enabled(ui->preview->GetDisplay(), enable); + ui->previewContainer->setVisible(enable); + ui->previewDisabledWidget->setVisible(!enable); +} + +void OBSBasic::TogglePreview() +{ + previewEnabled = !previewEnabled; + EnablePreviewDisplay(previewEnabled); +} + +void OBSBasic::EnablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = true; + EnablePreviewDisplay(true); +} + +void OBSBasic::DisablePreview() +{ + if (previewProgramMode) + return; + + previewEnabled = false; + EnablePreviewDisplay(false); +} + +void OBSBasic::EnablePreviewProgram() +{ + SetPreviewProgramMode(true); +} + +void OBSBasic::DisablePreviewProgram() +{ + SetPreviewProgramMode(false); +} + +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 OBSBasic::Nudge(int dist, MoveDir dir) +{ + if (ui->preview->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; + } + + if (!recent_nudge) { + recent_nudge = true; + OBSDataAutoRelease wrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string undo_data(obs_data_get_json(wrapper)); + + nudge_timer = new QTimer; + QObject::connect(nudge_timer, &QTimer::timeout, [this, &recent_nudge = recent_nudge, undo_data]() { + OBSDataAutoRelease rwrapper = obs_scene_save_transform_states(GetCurrentScene(), true); + std::string redo_data(obs_data_get_json(rwrapper)); + + undo_s.add_action(QTStr("Undo.Transform").arg(obs_source_get_name(GetCurrentSceneSource())), + undo_redo, undo_redo, undo_data, redo_data); + + recent_nudge = false; + }); + connect(nudge_timer, &QTimer::timeout, nudge_timer, &QTimer::deleteLater); + nudge_timer->setSingleShot(true); + } + + if (nudge_timer) { + nudge_timer->stop(); + nudge_timer->start(1000); + } else { + blog(LOG_ERROR, "No nudge timer!"); + } + + obs_scene_enum_items(GetCurrentScene(), nudge_callback, &offset); +} + +void OBSBasic::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 *OBSBasic::OpenProjector(obs_source_t *source, int monitor, ProjectorType type) +{ + /* seriously? 10 monitors? */ + if (monitor > 9 || monitor > QGuiApplication::screens().size() - 1) + return nullptr; + + bool closeProjectors = config_get_bool(App()->GetUserConfig(), "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(nullptr, source, monitor, type); + + projectors.emplace_back(projector); + + return projector; +} + +void OBSBasic::OpenStudioProgramProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OpenProjector(obs_sceneitem_get_source(item), monitor, ProjectorType::Source); +} + +void OBSBasic::OpenMultiviewProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OpenProjector(nullptr, monitor, ProjectorType::Multiview); +} + +void OBSBasic::OpenSceneProjector() +{ + int monitor = sender()->property("monitor").toInt(); + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OpenProjector(obs_scene_get_source(scene), monitor, ProjectorType::Scene); +} + +void OBSBasic::OpenStudioProgramWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::StudioProgram); +} + +void OBSBasic::OpenPreviewWindow() +{ + OpenProjector(nullptr, -1, ProjectorType::Preview); +} + +void OBSBasic::OpenSourceWindow() +{ + OBSSceneItem item = GetCurrentSceneItem(); + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + OpenProjector(obs_sceneitem_get_source(item), -1, ProjectorType::Source); +} + +void OBSBasic::OpenSceneWindow() +{ + OBSScene scene = GetCurrentScene(); + if (!scene) + return; + + OBSSource source = obs_scene_get_source(scene); + + OpenProjector(obs_scene_get_source(scene), -1, ProjectorType::Scene); +} + +void OBSBasic::OpenSavedProjectors() +{ + for (SavedProjectorInfo *info : savedProjectorsArray) { + OpenSavedProjector(info); + } +} + +void OBSBasic::OpenSavedProjector(SavedProjectorInfo *info) +{ + if (info) { + OBSProjector *projector = nullptr; + switch (info->type) { + case ProjectorType::Source: + case ProjectorType::Scene: { + OBSSourceAutoRelease source = obs_get_source_by_name(info->name.c_str()); + if (!source) + return; + + projector = OpenProjector(source, info->monitor, info->type); + break; + } + default: { + projector = OpenProjector(nullptr, info->monitor, info->type); + break; + } + } + + if (projector && !info->geometry.empty() && info->monitor < 0) { + QByteArray byteArray = QByteArray::fromBase64(QByteArray(info->geometry.c_str())); + projector->restoreGeometry(byteArray); + + if (!WindowPositionValid(projector->normalGeometry())) { + QRect rect = QGuiApplication::primaryScreen()->geometry(); + projector->setGeometry( + QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), rect)); + } + + if (info->alwaysOnTopOverridden) + projector->SetIsAlwaysOnTop(info->alwaysOnTop, true); + } + } +} + +void OBSBasic::on_actionFullscreenInterface_triggered() +{ + if (!isFullScreen()) + showFullScreen(); + else + showNormal(); +} + +void OBSBasic::UpdateTitleBar() +{ + stringstream name; + + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + const char *sceneCollection = config_get_string(App()->GetUserConfig(), "Basic", "SceneCollection"); + + name << "OBS "; + if (previewProgramMode) + name << "Studio "; + + name << App()->GetVersionString(false); + if (safe_mode) + name << " (" << Str("TitleBar.SafeMode") << ")"; + if (App()->IsPortableMode()) + name << " - " << Str("TitleBar.PortableMode"); + + name << " - " << Str("TitleBar.Profile") << ": " << profile; + name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection; + + setWindowTitle(QT_UTF8(name.str().c_str())); +} + +int OBSBasic::GetProfilePath(char *path, size_t size, const char *file) const +{ + char profiles_path[512]; + const char *profile = config_get_string(App()->GetUserConfig(), "Basic", "ProfileDir"); + int ret; + + if (!profile) + return -1; + if (!path) + return -1; + if (!file) + file = ""; + + ret = GetAppConfigPath(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); +} + +void OBSBasic::on_resetDocks_triggered(bool force) +{ + /* prune deleted extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + +#ifdef BROWSER_AVAILABLE + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size() || extraBrowserDocks.size()) && + !force) +#else + if ((oldExtraDocks.size() || extraDocks.size() || extraCustomDocks.size()) && !force) +#endif + { + QMessageBox::StandardButton button = + OBSMessageBox::question(this, QTStr("ResetUIWarning.Title"), QTStr("ResetUIWarning.Text")); + + if (button == QMessageBox::No) + return; + } + + /* undock/hide/center extra docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (oldExtraDocks[i]) { + oldExtraDocks[i]->setVisible(true); + oldExtraDocks[i]->setFloating(true); + oldExtraDocks[i]->move(frameGeometry().topLeft() + rect().center() - + oldExtraDocks[i]->rect().center()); + oldExtraDocks[i]->setVisible(false); + } + } + +#define RESET_DOCKLIST(dockList) \ + for (int i = dockList.size() - 1; i >= 0; i--) { \ + dockList[i]->setVisible(true); \ + dockList[i]->setFloating(true); \ + dockList[i]->move(frameGeometry().topLeft() + rect().center() - dockList[i]->rect().center()); \ + dockList[i]->setVisible(false); \ + } + + RESET_DOCKLIST(extraDocks) + RESET_DOCKLIST(extraCustomDocks) +#ifdef BROWSER_AVAILABLE + RESET_DOCKLIST(extraBrowserDocks) +#endif +#undef RESET_DOCKLIST + + restoreState(startingDockLayout); + + int cx = width(); + int cy = height(); + + int cx22_5 = cx * 225 / 1000; + int cx5 = cx * 5 / 100; + int cx21 = cx * 21 / 100; + + cy = cy * 225 / 1000; + + int mixerSize = cx - (cx22_5 * 2 + cx5 + cx21); + + QList docks{ui->scenesDock, ui->sourcesDock, ui->mixerDock, ui->transitionsDock, controlsDock}; + + QList sizes{cx22_5, cx22_5, mixerSize, cx5, cx21}; + + ui->scenesDock->setVisible(true); + ui->sourcesDock->setVisible(true); + ui->mixerDock->setVisible(true); + ui->transitionsDock->setVisible(true); + controlsDock->setVisible(true); + statsDock->setVisible(false); + statsDock->setFloating(true); + + resizeDocks(docks, {cy, cy, cy, cy, cy}, Qt::Vertical); + resizeDocks(docks, sizes, Qt::Horizontal); + + activateWindow(); +} + +void OBSBasic::on_lockDocks_toggled(bool lock) +{ + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + QDockWidget::DockWidgetFeatures mainFeatures = features; + mainFeatures &= ~QDockWidget::QDockWidget::DockWidgetClosable; + + ui->scenesDock->setFeatures(mainFeatures); + ui->sourcesDock->setFeatures(mainFeatures); + ui->mixerDock->setFeatures(mainFeatures); + ui->transitionsDock->setFeatures(mainFeatures); + controlsDock->setFeatures(mainFeatures); + statsDock->setFeatures(features); + + for (int i = extraDocks.size() - 1; i >= 0; i--) + extraDocks[i]->setFeatures(features); + + for (int i = extraCustomDocks.size() - 1; i >= 0; i--) + extraCustomDocks[i]->setFeatures(features); + +#ifdef BROWSER_AVAILABLE + for (int i = extraBrowserDocks.size() - 1; i >= 0; i--) + extraBrowserDocks[i]->setFeatures(features); +#endif + + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } else { + oldExtraDocks[i]->setFeatures(features); + } + } +} + +void OBSBasic::on_sideDocks_toggled(bool side) +{ + if (side) { + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + } else { + setCorner(Qt::TopLeftCorner, Qt::TopDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::BottomDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); + } +} + +void OBSBasic::on_resetUI_triggered() +{ + on_resetDocks_triggered(); + + ui->toggleListboxToolbars->setChecked(true); + ui->toggleContextBar->setChecked(true); + ui->toggleSourceIcons->setChecked(true); + ui->toggleStatusBar->setChecked(true); + ui->scenes->SetGridMode(false); + ui->actionSceneListMode->setChecked(true); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "gridMode", false); +} + +void OBSBasic::on_multiviewProjectorWindowed_triggered() +{ + OpenProjector(nullptr, -1, ProjectorType::Multiview); +} + +void OBSBasic::on_toggleListboxToolbars_toggled(bool visible) +{ + ui->sourcesToolbar->setVisible(visible); + ui->scenesToolbar->setVisible(visible); + ui->mixerToolbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowListboxToolbars", visible); +} + +void OBSBasic::ShowContextBar() +{ + on_toggleContextBar_toggled(true); + ui->toggleContextBar->setChecked(true); +} + +void OBSBasic::HideContextBar() +{ + on_toggleContextBar_toggled(false); + ui->toggleContextBar->setChecked(false); +} + +void OBSBasic::on_toggleContextBar_toggled(bool visible) +{ + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowContextToolbars", visible); + this->ui->contextContainer->setVisible(visible); + UpdateContextBar(true); +} + +void OBSBasic::on_toggleStatusBar_toggled(bool visible) +{ + ui->statusbar->setVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowStatusBar", visible); +} + +void OBSBasic::on_toggleSourceIcons_toggled(bool visible) +{ + ui->sources->SetIconsVisible(visible); + if (advAudioWindow != nullptr) + advAudioWindow->SetIconsVisible(visible); + + config_set_bool(App()->GetUserConfig(), "BasicWindow", "ShowSourceIcons", visible); +} + +void OBSBasic::on_actionLockPreview_triggered() +{ + ui->preview->ToggleLocked(); + ui->actionLockPreview->setChecked(ui->preview->Locked()); +} + +void OBSBasic::on_scalingMenu_aboutToShow() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + QAction *action = ui->actionScaleCanvas; + QString text = QTStr("Basic.MainMenu.Edit.Scale.Canvas"); + text = text.arg(QString::number(ovi.base_width), QString::number(ovi.base_height)); + action->setText(text); + + action = ui->actionScaleOutput; + text = QTStr("Basic.MainMenu.Edit.Scale.Output"); + text = text.arg(QString::number(ovi.output_width), QString::number(ovi.output_height)); + action->setText(text); + action->setVisible(!(ovi.output_width == ovi.base_width && ovi.output_height == ovi.base_height)); + + UpdatePreviewScalingMenu(); +} + +void OBSBasic::on_actionScaleWindow_triggered() +{ + ui->preview->SetFixedScaling(false); + ui->preview->ResetScrollingOffset(); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleCanvas_triggered() +{ + ui->preview->SetFixedScaling(true); + ui->preview->SetScalingLevel(0); + + emit ui->preview->DisplayResized(); +} + +void OBSBasic::on_actionScaleOutput_triggered() +{ + obs_video_info ovi; + obs_get_video_info(&ovi); + + ui->preview->SetFixedScaling(true); + float scalingAmount = float(ovi.output_width) / float(ovi.base_width); + // log base ZOOM_SENSITIVITY of x = log(x) / log(ZOOM_SENSITIVITY) + int32_t approxScalingLevel = int32_t(round(log(scalingAmount) / log(ZOOM_SENSITIVITY))); + ui->preview->SetScalingLevelAndAmount(approxScalingLevel, scalingAmount); + emit ui->preview->DisplayResized(); +} + +void OBSBasic::SetShowing(bool showing) +{ + if (!showing && isVisible()) { + config_set_string(App()->GetUserConfig(), "BasicWindow", "geometry", + saveGeometry().toBase64().constData()); + + /* hide all visible child dialogs */ + visDlgPositions.clear(); + if (!visDialogs.isEmpty()) { + for (QDialog *dlg : visDialogs) { + visDlgPositions.append(dlg->pos()); + dlg->hide(); + } + } + + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Show")); + QTimer::singleShot(0, this, &OBSBasic::hide); + + if (previewEnabled) + EnablePreviewDisplay(false); + +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + + } else if (showing && !isVisible()) { + if (showHide) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + QTimer::singleShot(0, this, &OBSBasic::show); + + if (previewEnabled) + EnablePreviewDisplay(true); + +#ifdef __APPLE__ + EnableOSXDockIcon(true); +#endif + + /* raise and activate window to ensure it is on top */ + raise(); + activateWindow(); + + /* show all child dialogs that was visible earlier */ + if (!visDialogs.isEmpty()) { + for (int i = 0; i < visDialogs.size(); ++i) { + QDialog *dlg = visDialogs[i]; + dlg->move(visDlgPositions[i]); + dlg->show(); + } + } + + /* Unminimize window if it was hidden to tray instead of task + * bar. */ + if (sysTrayMinimizeToTray()) { + Qt::WindowStates state; + state = windowState() & ~Qt::WindowMinimized; + state |= Qt::WindowActive; + setWindowState(state); + } + } +} + +void OBSBasic::ToggleShowHide() +{ + bool showing = isVisible(); + if (showing) { + /* check for modal dialogs */ + EnumDialogs(); + if (!modalDialogs.isEmpty() || !visMsgBoxes.isEmpty()) + return; + } + SetShowing(!showing); +} + +void OBSBasic::SystemTrayInit() +{ +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs.png"); +#endif + trayIcon.reset(new QSystemTrayIcon(QIcon::fromTheme("obs-tray", trayIconFile), this)); + trayIcon->setToolTip("OBS Studio"); + + showHide = new QAction(QTStr("Basic.SystemTray.Show"), trayIcon.data()); + sysTrayStream = + new QAction(StreamingActive() ? QTStr("Basic.Main.StopStreaming") : QTStr("Basic.Main.StartStreaming"), + trayIcon.data()); + sysTrayRecord = + new QAction(RecordingActive() ? QTStr("Basic.Main.StopRecording") : QTStr("Basic.Main.StartRecording"), + trayIcon.data()); + sysTrayReplayBuffer = new QAction(ReplayBufferActive() ? QTStr("Basic.Main.StopReplayBuffer") + : QTStr("Basic.Main.StartReplayBuffer"), + trayIcon.data()); + sysTrayVirtualCam = new QAction(VirtualCamActive() ? QTStr("Basic.Main.StopVirtualCam") + : QTStr("Basic.Main.StartVirtualCam"), + trayIcon.data()); + exit = new QAction(QTStr("Exit"), trayIcon.data()); + + trayMenu = new QMenu; + previewProjector = new QMenu(QTStr("PreviewProjector")); + studioProgramProjector = new QMenu(QTStr("StudioProgramProjector")); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + trayMenu->addAction(showHide); + trayMenu->addSeparator(); + trayMenu->addMenu(previewProjector); + trayMenu->addMenu(studioProgramProjector); + trayMenu->addSeparator(); + trayMenu->addAction(sysTrayStream); + trayMenu->addAction(sysTrayRecord); + trayMenu->addAction(sysTrayReplayBuffer); + trayMenu->addAction(sysTrayVirtualCam); + trayMenu->addSeparator(); + trayMenu->addAction(exit); + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + if (outputHandler && !outputHandler->replayBuffer) + sysTrayReplayBuffer->setEnabled(false); + + sysTrayVirtualCam->setEnabled(vcamEnabled); + + if (Active()) + OnActivate(true); + + connect(trayIcon.data(), &QSystemTrayIcon::activated, this, &OBSBasic::IconActivated); + connect(showHide, &QAction::triggered, this, &OBSBasic::ToggleShowHide); + connect(sysTrayStream, &QAction::triggered, this, &OBSBasic::StreamActionTriggered); + connect(sysTrayRecord, &QAction::triggered, this, &OBSBasic::RecordActionTriggered); + connect(sysTrayReplayBuffer.data(), &QAction::triggered, this, &OBSBasic::ReplayBufferActionTriggered); + connect(sysTrayVirtualCam.data(), &QAction::triggered, this, &OBSBasic::VirtualCamActionTriggered); + connect(exit, &QAction::triggered, this, &OBSBasic::close); +} + +void OBSBasic::IconActivated(QSystemTrayIcon::ActivationReason reason) +{ + // Refresh projector list + previewProjector->clear(); + studioProgramProjector->clear(); + AddProjectorMenuMonitors(previewProjector, this, &OBSBasic::OpenPreviewProjector); + AddProjectorMenuMonitors(studioProgramProjector, this, &OBSBasic::OpenStudioProgramProjector); + +#ifdef __APPLE__ + UNUSED_PARAMETER(reason); +#else + if (reason == QSystemTrayIcon::Trigger) { + EnablePreviewDisplay(previewEnabled && !isVisible()); + ToggleShowHide(); + } +#endif +} + +void OBSBasic::SysTrayNotify(const QString &text, QSystemTrayIcon::MessageIcon n) +{ + if (trayIcon && trayIcon->isVisible() && QSystemTrayIcon::supportsMessages()) { + QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon(n); + trayIcon->showMessage("OBS Studio", text, icon, 10000); + } +} + +void OBSBasic::SystemTray(bool firstStarted) +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) + return; + if (!trayIcon && !firstStarted) + return; + + bool sysTrayWhenStarted = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayWhenStarted"); + bool sysTrayEnabled = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayEnabled"); + + if (firstStarted) + SystemTrayInit(); + + if (!sysTrayEnabled) { + trayIcon->hide(); + } else { + trayIcon->show(); + if (firstStarted && (sysTrayWhenStarted || opt_minimize_tray)) { + EnablePreviewDisplay(false); +#ifdef __APPLE__ + EnableOSXDockIcon(false); +#endif + opt_minimize_tray = false; + } + } + + if (isVisible()) + showHide->setText(QTStr("Basic.SystemTray.Hide")); + else + showHide->setText(QTStr("Basic.SystemTray.Show")); +} + +bool OBSBasic::sysTrayMinimizeToTray() +{ + return config_get_bool(App()->GetUserConfig(), "BasicWindow", "SysTrayMinimizeToTray"); +} + +void OBSBasic::on_actionMainUndo_triggered() +{ + undo_s.undo(); +} + +void OBSBasic::on_actionMainRedo_triggered() +{ + undo_s.redo(); +} + +void OBSBasic::on_actionCopySource_triggered() +{ + clipboard.clear(); + + for (auto &selectedSource : GetAllSelectedSourceItems()) { + OBSSceneItem item = ui->sources->Get(selectedSource.row()); + if (!item) + continue; + + OBSSource source = obs_sceneitem_get_source(item); + + SourceCopyInfo copyInfo; + copyInfo.weak_source = OBSGetWeakRef(source); + obs_sceneitem_get_info2(item, ©Info.transform); + obs_sceneitem_get_crop(item, ©Info.crop); + copyInfo.blend_method = obs_sceneitem_get_blending_method(item); + copyInfo.blend_mode = obs_sceneitem_get_blending_mode(item); + copyInfo.visible = obs_sceneitem_visible(item); + + clipboard.push_back(copyInfo); + } + + UpdateEditMenu(); +} + +void OBSBasic::on_actionPasteRef_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + OBSScene scene = GetCurrentScene(); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + + OBSSource source = OBSGetStrongRef(copyInfo.weak_source); + if (!source) + continue; + + const char *name = obs_source_get_name(source); + + /* do not allow duplicate refs of the same group in the same + * scene */ + if (!!obs_scene_get_group(scene, name)) { + continue; + } + + OBSBasicSourceSelect::SourcePaste(copyInfo, false); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSourceRef"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::on_actionPasteDup_triggered() +{ + OBSSource scene_source = GetCurrentSceneSource(); + OBSData undo_data = BackupScene(scene_source); + + undo_s.push_disabled(); + + for (size_t i = clipboard.size(); i > 0; i--) { + SourceCopyInfo ©Info = clipboard[i - 1]; + OBSBasicSourceSelect::SourcePaste(copyInfo, true); + } + + undo_s.pop_disabled(); + + QString action_name = QTStr("Undo.PasteSource"); + const char *scene_name = obs_source_get_name(scene_source); + + OBSData redo_data = BackupScene(scene_source); + CreateSceneUndoRedoAction(action_name.arg(scene_name), undo_data, redo_data); +} + +void OBSBasic::SourcePasteFilters(OBSSource source, OBSSource dstSource) +{ + if (source == dstSource) + return; + + OBSDataArrayAutoRelease undo_array = obs_source_backup_filters(dstSource); + obs_source_copy_filters(dstSource, source); + OBSDataArrayAutoRelease redo_array = obs_source_backup_filters(dstSource); + + const char *srcName = obs_source_get_name(source); + const char *dstName = obs_source_get_name(dstSource); + QString text = QTStr("Undo.Filters.Paste.Multiple").arg(srcName, dstName); + + CreateFilterPasteUndoRedoAction(text, dstSource, undo_array, redo_array); +} + +void OBSBasic::AudioMixerCopyFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *source = vol->GetSource(); + + copyFiltersSource = obs_source_get_weak_source(source); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::AudioMixerPasteFilters() +{ + QAction *action = reinterpret_cast(sender()); + VolControl *vol = action->property("volControl").value(); + obs_source_t *dstSource = vol->GetSource(); + + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::SceneCopyFilters() +{ + copyFiltersSource = obs_source_get_weak_source(GetCurrentSceneSource()); + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::ScenePasteFilters() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSource dstSource = GetCurrentSceneSource(); + + SourcePasteFilters(source.Get(), dstSource); +} + +void OBSBasic::on_actionCopyFilters_triggered() +{ + OBSSceneItem item = GetCurrentSceneItem(); + + if (!item) + return; + + OBSSource source = obs_sceneitem_get_source(item); + + copyFiltersSource = obs_source_get_weak_source(source); + + ui->actionPasteFilters->setEnabled(true); +} + +void OBSBasic::CreateFilterPasteUndoRedoAction(const QString &text, obs_source_t *source, obs_data_array_t *undo_array, + obs_data_array_t *redo_array) +{ + auto undo_redo = [this](const std::string &json) { + OBSDataAutoRelease data = obs_data_create_from_json(json.c_str()); + OBSDataArrayAutoRelease array = obs_data_get_array(data, "array"); + OBSSourceAutoRelease source = obs_get_source_by_uuid(obs_data_get_string(data, "uuid")); + + obs_source_restore_filters(source, array); + + if (filters) + filters->UpdateSource(source); + }; + + const char *uuid = obs_source_get_uuid(source); + + OBSDataAutoRelease undo_data = obs_data_create(); + OBSDataAutoRelease redo_data = obs_data_create(); + obs_data_set_array(undo_data, "array", undo_array); + obs_data_set_array(redo_data, "array", redo_array); + obs_data_set_string(undo_data, "uuid", uuid); + obs_data_set_string(redo_data, "uuid", uuid); + + undo_s.add_action(text, undo_redo, undo_redo, obs_data_get_json(undo_data), obs_data_get_json(redo_data)); +} + +void OBSBasic::on_actionPasteFilters_triggered() +{ + OBSSourceAutoRelease source = obs_weak_source_get_source(copyFiltersSource); + + OBSSceneItem sceneItem = GetCurrentSceneItem(); + OBSSource dstSource = obs_sceneitem_get_source(sceneItem); + + SourcePasteFilters(source.Get(), dstSource); +} + +static void ConfirmColor(SourceTree *sources, const QColor &color, QModelIndexList selectedItems) +{ + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", 1); + obs_data_set_string(privData, "color", QT_TO_UTF8(color.name(QColor::HexArgb))); + } +} + +void OBSBasic::ColorChange() +{ + QModelIndexList selectedItems = ui->sources->selectionModel()->selectedIndexes(); + QAction *action = qobject_cast(sender()); + QPushButton *colorButton = qobject_cast(sender()); + + if (selectedItems.count() == 0) + return; + + if (colorButton) { + int preset = colorButton->property("bgColor").value(); + + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet(""); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset + 1); + obs_data_set_string(privData, "color", ""); + } + + for (int i = 1; i < 9; i++) { + stringstream button; + button << "preset" << i; + QPushButton *cButton = + colorButton->parentWidget()->findChild(button.str().c_str()); + cButton->setStyleSheet("border: 1px solid black"); + } + + colorButton->setStyleSheet("border: 2px solid black"); + } else if (action) { + int preset = action->property("bgColor").value(); + + if (preset == 1) { + OBSSceneItem curSceneItem = GetCurrentSceneItem(); + SourceTreeItem *curTreeItem = GetItemWidgetFromSceneItem(curSceneItem); + OBSDataAutoRelease curPrivData = obs_sceneitem_get_private_settings(curSceneItem); + + int oldPreset = obs_data_get_int(curPrivData, "color-preset"); + const QString oldSheet = curTreeItem->styleSheet(); + + auto liveChangeColor = [=](const QColor &color) { + if (color.isValid()) { + curTreeItem->setStyleSheet("background: " + color.name(QColor::HexArgb)); + } + }; + + auto changedColor = [=](const QColor &color) { + if (color.isValid()) { + ConfirmColor(ui->sources, color, selectedItems); + } + }; + + auto rejected = [=]() { + if (oldPreset == 1) { + curTreeItem->setStyleSheet(oldSheet); + curTreeItem->setProperty("bgColor", 0); + } else if (oldPreset == 0) { + curTreeItem->setStyleSheet("background: none"); + curTreeItem->setProperty("bgColor", 0); + } else { + curTreeItem->setStyleSheet(""); + curTreeItem->setProperty("bgColor", oldPreset - 1); + } + + curTreeItem->style()->unpolish(curTreeItem); + curTreeItem->style()->polish(curTreeItem); + }; + + QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; + + const char *oldColor = obs_data_get_string(curPrivData, "color"); + const char *customColor = *oldColor != 0 ? oldColor : "#55FF0000"; +#ifdef __linux__ + // TODO: Revisit hang on Ubuntu with native dialog + options |= QColorDialog::DontUseNativeDialog; +#endif + + QColorDialog *colorDialog = new QColorDialog(this); + colorDialog->setOptions(options); + colorDialog->setCurrentColor(QColor(customColor)); + connect(colorDialog, &QColorDialog::currentColorChanged, liveChangeColor); + connect(colorDialog, &QColorDialog::colorSelected, changedColor); + connect(colorDialog, &QColorDialog::rejected, rejected); + colorDialog->open(); + } else { + for (int x = 0; x < selectedItems.count(); x++) { + SourceTreeItem *treeItem = ui->sources->GetItemWidget(selectedItems[x].row()); + treeItem->setStyleSheet("background: none"); + treeItem->setProperty("bgColor", preset); + treeItem->style()->unpolish(treeItem); + treeItem->style()->polish(treeItem); + + OBSSceneItem sceneItem = ui->sources->Get(selectedItems[x].row()); + OBSDataAutoRelease privData = obs_sceneitem_get_private_settings(sceneItem); + obs_data_set_int(privData, "color-preset", preset); + obs_data_set_string(privData, "color", ""); + } + } + } +} + +SourceTreeItem *OBSBasic::GetItemWidgetFromSceneItem(obs_sceneitem_t *sceneItem) +{ + int i = 0; + SourceTreeItem *treeItem = ui->sources->GetItemWidget(i); + OBSSceneItem item = ui->sources->Get(i); + int64_t id = obs_sceneitem_get_id(sceneItem); + while (treeItem && obs_sceneitem_get_id(item) != id) { + i++; + treeItem = ui->sources->GetItemWidget(i); + item = ui->sources->Get(i); + } + if (treeItem) + return treeItem; + + return nullptr; +} + +void OBSBasic::on_autoConfigure_triggered() +{ + AutoConfig test(this); + test.setModal(true); + test.show(); + test.exec(); +} + +void OBSBasic::on_stats_triggered() +{ + if (!stats.isNull()) { + stats->show(); + stats->raise(); + return; + } + + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->show(); + stats = statsDlg; +} + +void OBSBasic::on_actionShowAbout_triggered() +{ + if (about) + about->close(); + + about = new OBSAbout(this); + about->show(); + + about->setAttribute(Qt::WA_DeleteOnClose, true); +} + +void OBSBasic::ResizeOutputSizeOfSource() +{ + if (obs_video_active()) + return; + + QMessageBox resize_output(this); + resize_output.setText(QTStr("ResizeOutputSizeOfSource.Text") + "\n\n" + + QTStr("ResizeOutputSizeOfSource.Continue")); + QAbstractButton *Yes = resize_output.addButton(QTStr("Yes"), QMessageBox::YesRole); + resize_output.addButton(QTStr("No"), QMessageBox::NoRole); + resize_output.setIcon(QMessageBox::Warning); + resize_output.setWindowTitle(QTStr("ResizeOutputSizeOfSource")); + resize_output.exec(); + + if (resize_output.clickedButton() != Yes) + return; + + OBSSource source = obs_sceneitem_get_source(GetCurrentSceneItem()); + + int width = obs_source_get_width(source); + int height = obs_source_get_height(source); + + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); + + ResetVideo(); + ResetOutputs(); + activeConfiguration.SaveSafe("tmp"); + on_actionFitToScreen_triggered(); +} + +QAction *OBSBasic::AddDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairOldExtraDockName); + +#ifdef BROWSER_AVAILABLE + QAction *action = new QAction(dock->windowTitle(), ui->menuDocks); + + if (!extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, action); + else + ui->menuDocks->addAction(action); +#else + QAction *action = ui->menuDocks->addAction(dock->windowTitle()); +#endif + action->setCheckable(true); + assignDockToggle(dock, action); + oldExtraDocks.push_back(dock); + oldExtraDockNames.push_back(dock->objectName()); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + + /* prune deleted docks */ + for (int i = oldExtraDocks.size() - 1; i >= 0; i--) { + if (!oldExtraDocks[i]) { + oldExtraDocks.removeAt(i); + oldExtraDockNames.removeAt(i); + } + } + + return action; +} + +void OBSBasic::RepairOldExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = oldExtraDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The dock '%s' got its object name restored", QT_TO_UTF8(oldExtraDockNames[idx])); + + dock->setObjectName(oldExtraDockNames[idx]); +} + +void OBSBasic::AddDockWidget(QDockWidget *dock, Qt::DockWidgetArea area, bool extraBrowser) +{ + if (dock->objectName().isEmpty()) + return; + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + setupDockAction(dock); + dock->setFeatures(features); + addDockWidget(area, dock); + +#ifdef BROWSER_AVAILABLE + if (extraBrowser && extraBrowserMenuDocksSeparator.isNull()) + extraBrowserMenuDocksSeparator = ui->menuDocks->addSeparator(); + + if (!extraBrowser && !extraBrowserMenuDocksSeparator.isNull()) + ui->menuDocks->insertAction(extraBrowserMenuDocksSeparator, dock->toggleViewAction()); + else + ui->menuDocks->addAction(dock->toggleViewAction()); + + if (extraBrowser) + return; +#else + UNUSED_PARAMETER(extraBrowser); + + ui->menuDocks->addAction(dock->toggleViewAction()); +#endif + + extraDockNames.push_back(dock->objectName()); + extraDocks.push_back(std::shared_ptr(dock)); +} + +void OBSBasic::RemoveDockWidget(const QString &name) +{ + if (extraDockNames.contains(name)) { + int idx = extraDockNames.indexOf(name); + extraDockNames.removeAt(idx); + extraDocks[idx].reset(); + extraDocks.removeAt(idx); + } else if (extraCustomDockNames.contains(name)) { + int idx = extraCustomDockNames.indexOf(name); + extraCustomDockNames.removeAt(idx); + removeDockWidget(extraCustomDocks[idx]); + extraCustomDocks.removeAt(idx); + } +} + +bool OBSBasic::IsDockObjectNameUsed(const QString &name) +{ + QStringList list; + list << "scenesDock" + << "sourcesDock" + << "mixerDock" + << "transitionsDock" + << "controlsDock" + << "statsDock"; + list << oldExtraDockNames; + list << extraDockNames; + list << extraCustomDockNames; + + return list.contains(name); +} + +void OBSBasic::AddCustomDockWidget(QDockWidget *dock) +{ + // Prevent the object name from being changed + connect(dock, &QObject::objectNameChanged, this, &OBSBasic::RepairCustomExtraDockName); + + bool lock = ui->lockDocks->isChecked(); + QDockWidget::DockWidgetFeatures features = + lock ? QDockWidget::NoDockWidgetFeatures + : (QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); + + dock->setFeatures(features); + addDockWidget(Qt::RightDockWidgetArea, dock); + + extraCustomDockNames.push_back(dock->objectName()); + extraCustomDocks.push_back(dock); +} + +void OBSBasic::RepairCustomExtraDockName() +{ + QDockWidget *dock = reinterpret_cast(sender()); + int idx = extraCustomDocks.indexOf(dock); + QSignalBlocker block(dock); + + if (idx == -1) { + blog(LOG_WARNING, "A custom dock got its object name changed"); + return; + } + + blog(LOG_WARNING, "The custom dock '%s' got its object name restored", QT_TO_UTF8(extraCustomDockNames[idx])); + + dock->setObjectName(extraCustomDockNames[idx]); +} + +OBSBasic *OBSBasic::Get() +{ + return reinterpret_cast(App()->GetMainWindow()); +} + +bool OBSBasic::StreamingActive() +{ + if (!outputHandler) + return false; + return outputHandler->StreamingActive(); +} + +bool OBSBasic::RecordingActive() +{ + if (!outputHandler) + return false; + return outputHandler->RecordingActive(); +} + +bool OBSBasic::ReplayBufferActive() +{ + if (!outputHandler) + return false; + return outputHandler->ReplayBufferActive(); +} + +bool OBSBasic::VirtualCamActive() +{ + if (!outputHandler) + return false; + return outputHandler->VirtualCamActive(); +} + +SceneRenameDelegate::SceneRenameDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + +void SceneRenameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QStyledItemDelegate::setEditorData(editor, index); + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->selectAll(); +} + +bool SceneRenameDelegate::eventFilter(QObject *editor, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + switch (keyEvent->key()) { + case Qt::Key_Escape: { + QLineEdit *lineEdit = qobject_cast(editor); + if (lineEdit) + lineEdit->undo(); + break; + } + case Qt::Key_Tab: + case Qt::Key_Backtab: + return false; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); +} + +void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) +{ + if (!error.isEmpty()) + return; + + patronJson = QT_TO_UTF8(text); +} + +void OBSBasic::PauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + os_atomic_set_bool(&recording_paused, true); + + emit RecordingPaused(); + + ui->statusbar->RecordingPaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusPaused); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/obs_paused_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/obs_paused.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-paused", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput || + !os_atomic_load_bool(&recording_paused)) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + os_atomic_set_bool(&recording_paused, false); + + emit RecordingUnpaused(); + + ui->statusbar->RecordingUnpaused(); + + TaskbarOverlaySetStatus(TaskbarOverlayStatusActive); + if (trayIcon && trayIcon->isVisible()) { +#ifdef __APPLE__ + QIcon trayIconFile = QIcon(":/res/images/tray_active_macos.svg"); + trayIconFile.setIsMask(true); +#else + QIcon trayIconFile = QIcon(":/res/images/tray_active.png"); +#endif + trayIcon->setIcon(QIcon::fromTheme("obs-tray-active", trayIconFile)); + } + + OnEvent(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::RecordPauseToggled() +{ + if (!isRecordingPausable || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (enable) + PauseRecording(); + else + UnpauseRecording(); +} + +void OBSBasic::UpdateIsRecordingPausable() +{ + const char *mode = config_get_string(activeConfiguration, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared = true; + + if (adv) { + const char *recType = config_get_string(activeConfiguration, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string(activeConfiguration, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string(activeConfiguration, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + isRecordingPausable = !shared; +} + +#define MBYTE (1024ULL * 1024ULL) +#define MBYTES_LEFT_STOP_REC 50ULL +#define MAX_BYTES_LEFT (MBYTES_LEFT_STOP_REC * MBYTE) + +const char *OBSBasic::GetCurrentOutputPath() +{ + const char *path = nullptr; + const char *mode = config_get_string(Config(), "Output", "Mode"); + + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + + if (strcmp(advanced_mode, "FFmpeg") == 0) { + path = config_get_string(Config(), "AdvOut", "FFFilePath"); + } else { + path = config_get_string(Config(), "AdvOut", "RecFilePath"); + } + } else { + path = config_get_string(Config(), "SimpleOutput", "FilePath"); + } + + return path; +} + +void OBSBasic::OutputPathInvalidMessage() +{ + blog(LOG_ERROR, "Recording stopped because of bad output path"); + + OBSMessageBox::critical(this, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); +} + +bool OBSBasic::IsFFmpegOutputToURL() const +{ + const char *mode = config_get_string(Config(), "Output", "Mode"); + if (strcmp(mode, "Advanced") == 0) { + const char *advanced_mode = config_get_string(Config(), "AdvOut", "RecType"); + if (strcmp(advanced_mode, "FFmpeg") == 0) { + bool is_local = config_get_bool(Config(), "AdvOut", "FFOutputToFile"); + if (!is_local) + return true; + } + } + + return false; +} + +bool OBSBasic::OutputPathValid() +{ + if (IsFFmpegOutputToURL()) + return true; + + const char *path = GetCurrentOutputPath(); + return path && *path && QDir(path).exists(); +} + +void OBSBasic::DiskSpaceMessage() +{ + blog(LOG_ERROR, "Recording stopped because of low disk space"); + + OBSMessageBox::critical(this, QTStr("Output.RecordNoSpace.Title"), QTStr("Output.RecordNoSpace.Msg")); +} + +bool OBSBasic::LowDiskSpace() +{ + const char *path; + + path = GetCurrentOutputPath(); + if (!path) + return false; + + uint64_t num_bytes = os_get_free_disk_space(path); + + if (num_bytes < (MAX_BYTES_LEFT)) + return true; + else + return false; +} + +void OBSBasic::CheckDiskSpaceRemaining() +{ + if (LowDiskSpace()) { + StopRecording(); + StopReplayBuffer(); + + DiskSpaceMessage(); + } +} + +void OBSBasic::ResetStatsHotkey() +{ + const QList list = findChildren(); + + for (OBSBasicStats *s : list) { + s->Reset(); + } +} + +void OBSBasic::on_OBSBasic_customContextMenuRequested(const QPoint &pos) +{ + QWidget *widget = childAt(pos); + const char *className = nullptr; + QString objName; + if (widget != nullptr) { + className = widget->metaObject()->className(); + objName = widget->objectName(); + } + + QPoint globalPos = mapToGlobal(pos); + if (className && strstr(className, "Dock") != nullptr && !objName.isEmpty()) { + if (objName.compare("scenesDock") == 0) { + ui->scenes->customContextMenuRequested(globalPos); + } else if (objName.compare("sourcesDock") == 0) { + ui->sources->customContextMenuRequested(globalPos); + } else if (objName.compare("mixerDock") == 0) { + StackedMixerAreaContextMenuRequested(); + } + } else if (!className) { + ui->menuDocks->exec(globalPos); + } +} + +void OBSBasic::UpdateProjectorHideCursor() +{ + for (size_t i = 0; i < projectors.size(); i++) + projectors[i]->SetHideCursor(); +} + +void OBSBasic::UpdateProjectorAlwaysOnTop(bool top) +{ + for (size_t i = 0; i < projectors.size(); i++) + SetAlwaysOnTop(projectors[i], top); +} + +void OBSBasic::ResetProjectors() +{ + OBSDataArrayAutoRelease savedProjectorList = SaveProjectors(); + ClearProjectors(); + LoadSavedProjectors(savedProjectorList); + OpenSavedProjectors(); +} + +void OBSBasic::on_sourcePropertiesButton_clicked() +{ + on_actionSourceProperties_triggered(); +} + +void OBSBasic::on_sourceFiltersButton_clicked() +{ + OpenFilters(); +} + +void OBSBasic::on_actionSceneFilters_triggered() +{ + OBSSource sceneSource = GetCurrentSceneSource(); + + if (sceneSource) + OpenFilters(sceneSource); +} + +void OBSBasic::on_sourceInteractButton_clicked() +{ + on_actionInteract_triggered(); +} + +void OBSBasic::ShowStatusBarMessage(const QString &message) +{ + ui->statusbar->clearMessage(); + ui->statusbar->showMessage(message, 10000); +} + +void OBSBasic::UpdatePreviewSafeAreas() +{ + drawSafeAreas = config_get_bool(App()->GetUserConfig(), "BasicWindow", "ShowSafeAreas"); +} + +void OBSBasic::UpdatePreviewOverflowSettings() +{ + bool hidden = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowHidden"); + bool select = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowSelectionHidden"); + bool always = config_get_bool(App()->GetUserConfig(), "BasicWindow", "OverflowAlwaysVisible"); + + ui->preview->SetOverflowHidden(hidden); + ui->preview->SetOverflowSelectionHidden(select); + ui->preview->SetOverflowAlwaysVisible(always); +} + +void OBSBasic::SetDisplayAffinity(QWindow *window) +{ + if (!SetDisplayAffinitySupported()) + return; + + bool hideFromCapture = config_get_bool(App()->GetUserConfig(), "BasicWindow", "HideOBSWindowsFromCapture"); + + // Don't hide projectors, those are designed to be visible / captured + if (window->property("isOBSProjectorWindow") == true) + return; + +#ifdef _WIN32 + HWND hwnd = (HWND)window->winId(); + + DWORD curAffinity; + if (GetWindowDisplayAffinity(hwnd, &curAffinity)) { + if (hideFromCapture && curAffinity != WDA_EXCLUDEFROMCAPTURE) + SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE); + else if (!hideFromCapture && curAffinity != WDA_NONE) + SetWindowDisplayAffinity(hwnd, WDA_NONE); + } + +#else + // TODO: Implement for other platforms if possible. Don't forget to + // implement SetDisplayAffinitySupported too! + UNUSED_PARAMETER(hideFromCapture); +#endif +} + +static inline QColor color_from_int(long long val) +{ + return QColor(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); +} + +QColor OBSBasic::GetSelectionColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectRed")); + } else { + return QColor::fromRgb(255, 0, 0); + } +} + +QColor OBSBasic::GetCropColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectGreen")); + } else { + return QColor::fromRgb(0, 255, 0); + } +} + +QColor OBSBasic::GetHoverColor() const +{ + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + return color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "SelectBlue")); + } else { + return QColor::fromRgb(0, 127, 255); + } +} + +void OBSBasic::UpdatePreviewSpacingHelpers() +{ + drawSpacingHelpers = config_get_bool(App()->GetUserConfig(), "BasicWindow", "SpacingHelpersEnabled"); +} + +float OBSBasic::GetDevicePixelRatio() +{ + return dpi; +} + +void OBSBasic::OnEvent(enum obs_frontend_event event) +{ + if (api) + api->on_event(event); +} + +void OBSBasic::UpdatePreviewScrollbars() +{ + if (!ui->preview->IsFixedScaling()) { + ui->previewXScrollBar->setRange(0, 0); + ui->previewYScrollBar->setRange(0, 0); + } +} + +void OBSBasic::on_previewXScrollBar_valueChanged(int value) +{ + emit PreviewXScrollBarMoved(value); +} + +void OBSBasic::on_previewYScrollBar_valueChanged(int value) +{ + emit PreviewYScrollBarMoved(value); +} + +void OBSBasic::PreviewScalingModeChanged(int value) +{ + switch (value) { + case 0: + on_actionScaleWindow_triggered(); + break; + case 1: + on_actionScaleCanvas_triggered(); + break; + case 2: + on_actionScaleOutput_triggered(); + break; + }; +} + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), result.optionValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() ? nullptr : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, QTStr("NoNameEntered.Title"), QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/frontend/widgets/OBSQTDisplay.cpp b/frontend/widgets/OBSQTDisplay.cpp new file mode 100644 index 000000000..f7c37ae9a --- /dev/null +++ b/frontend/widgets/OBSQTDisplay.cpp @@ -0,0 +1,243 @@ +#include "moc_qt-display.cpp" +#include "display-helpers.hpp" +#include +#include +#include +#include + +#include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#if !defined(_WIN32) && !defined(__APPLE__) +#include +#endif + +#ifdef ENABLE_WAYLAND +#include +#endif + +class SurfaceEventFilter : public QObject { + OBSQTDisplay *display; + +public: + SurfaceEventFilter(OBSQTDisplay *src) : QObject(src), display(src) {} + +protected: + bool eventFilter(QObject *obj, QEvent *event) override + { + bool result = QObject::eventFilter(obj, event); + QPlatformSurfaceEvent *surfaceEvent; + + switch (event->type()) { + case QEvent::PlatformSurface: + surfaceEvent = static_cast(event); + + switch (surfaceEvent->surfaceEventType()) { + case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed: + display->DestroyDisplay(); + break; + default: + break; + } + break; + default: + break; + } + + return result; + } +}; + +static inline long long color_to_int(const QColor &color) +{ + auto shift = [&](unsigned val, int shift) { + return ((val & 0xff) << shift); + }; + + return shift(color.red(), 0) | shift(color.green(), 8) | shift(color.blue(), 16) | shift(color.alpha(), 24); +} + +static inline QColor rgba_to_color(uint32_t rgba) +{ + return QColor::fromRgb(rgba & 0xFF, (rgba >> 8) & 0xFF, (rgba >> 16) & 0xFF, (rgba >> 24) & 0xFF); +} + +static bool QTToGSWindow(QWindow *window, gs_window &gswindow) +{ + bool success = true; + +#ifdef _WIN32 + gswindow.hwnd = (HWND)window->winId(); +#elif __APPLE__ + gswindow.view = (id)window->winId(); +#else + switch (obs_get_nix_platform()) { + case OBS_NIX_PLATFORM_X11_EGL: + gswindow.id = window->winId(); + gswindow.display = obs_get_nix_platform_display(); + break; +#ifdef ENABLE_WAYLAND + case OBS_NIX_PLATFORM_WAYLAND: { + QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); + gswindow.display = native->nativeResourceForWindow("surface", window); + success = gswindow.display != nullptr; + break; + } +#endif + default: + success = false; + break; + } +#endif + return success; +} + +OBSQTDisplay::OBSQTDisplay(QWidget *parent, Qt::WindowFlags flags) : QWidget(parent, flags) +{ + setAttribute(Qt::WA_PaintOnScreen); + setAttribute(Qt::WA_StaticContents); + setAttribute(Qt::WA_NoSystemBackground); + setAttribute(Qt::WA_OpaquePaintEvent); + setAttribute(Qt::WA_DontCreateNativeAncestors); + setAttribute(Qt::WA_NativeWindow); + + auto windowVisible = [this](bool visible) { + if (!visible) { +#if !defined(_WIN32) && !defined(__APPLE__) + display = nullptr; +#endif + return; + } + + if (!display) { + CreateDisplay(); + } else { + QSize size = GetPixelSize(this); + obs_display_resize(display, size.width(), size.height()); + } + }; + + auto screenChanged = [this](QScreen *) { + CreateDisplay(); + + QSize size = GetPixelSize(this); + obs_display_resize(display, size.width(), size.height()); + }; + + connect(windowHandle(), &QWindow::visibleChanged, windowVisible); + connect(windowHandle(), &QWindow::screenChanged, screenChanged); + + windowHandle()->installEventFilter(new SurfaceEventFilter(this)); +} + +QColor OBSQTDisplay::GetDisplayBackgroundColor() const +{ + return rgba_to_color(backgroundColor); +} + +void OBSQTDisplay::SetDisplayBackgroundColor(const QColor &color) +{ + uint32_t newBackgroundColor = (uint32_t)color_to_int(color); + + if (newBackgroundColor != backgroundColor) { + backgroundColor = newBackgroundColor; + UpdateDisplayBackgroundColor(); + } +} + +void OBSQTDisplay::UpdateDisplayBackgroundColor() +{ + obs_display_set_background_color(display, backgroundColor); +} + +void OBSQTDisplay::CreateDisplay() +{ + if (display) + return; + + if (destroying) + return; + + if (!windowHandle()->isExposed()) + return; + + QSize size = GetPixelSize(this); + + gs_init_data info = {}; + info.cx = size.width(); + info.cy = size.height(); + info.format = GS_BGRA; + info.zsformat = GS_ZS_NONE; + + if (!QTToGSWindow(windowHandle(), info.window)) + return; + + display = obs_display_create(&info, backgroundColor); + + emit DisplayCreated(this); +} + +void OBSQTDisplay::paintEvent(QPaintEvent *event) +{ + CreateDisplay(); + + QWidget::paintEvent(event); +} + +void OBSQTDisplay::moveEvent(QMoveEvent *event) +{ + QWidget::moveEvent(event); + + OnMove(); +} + +bool OBSQTDisplay::nativeEvent(const QByteArray &, void *message, qintptr *) +{ +#ifdef _WIN32 + const MSG &msg = *static_cast(message); + switch (msg.message) { + case WM_DISPLAYCHANGE: + OnDisplayChange(); + } +#else + UNUSED_PARAMETER(message); +#endif + + return false; +} + +void OBSQTDisplay::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + + CreateDisplay(); + + if (isVisible() && display) { + QSize size = GetPixelSize(this); + obs_display_resize(display, size.width(), size.height()); + } + + emit DisplayResized(); +} + +QPaintEngine *OBSQTDisplay::paintEngine() const +{ + return nullptr; +} + +void OBSQTDisplay::OnMove() +{ + if (display) + obs_display_update_color_space(display); +} + +void OBSQTDisplay::OnDisplayChange() +{ + if (display) + obs_display_update_color_space(display); +} diff --git a/UI/qt-display.hpp b/frontend/widgets/OBSQTDisplay.hpp similarity index 100% rename from UI/qt-display.hpp rename to frontend/widgets/OBSQTDisplay.hpp diff --git a/frontend/widgets/StatusBarWidget.cpp b/frontend/widgets/StatusBarWidget.cpp new file mode 100644 index 000000000..5bcae6f33 --- /dev/null +++ b/frontend/widgets/StatusBarWidget.cpp @@ -0,0 +1,601 @@ +#include +#include +#include "obs-app.hpp" +#include "window-basic-main.hpp" +#include "moc_window-basic-status-bar.cpp" +#include "window-basic-main-outputs.hpp" +#include "qt-wrappers.hpp" +#include "platform.hpp" + +#include "ui_StatusBarWidget.h" + +static constexpr int bitrateUpdateSeconds = 2; +static constexpr int congestionUpdateSeconds = 4; +static constexpr float excellentThreshold = 0.0f; +static constexpr float goodThreshold = 0.3333f; +static constexpr float mediocreThreshold = 0.6667f; +static constexpr float badThreshold = 1.0f; + +StatusBarWidget::StatusBarWidget(QWidget *parent) : QWidget(parent), ui(new Ui::StatusBarWidget) +{ + ui->setupUi(this); +} + +StatusBarWidget::~StatusBarWidget() {} + +OBSBasicStatusBar::OBSBasicStatusBar(QWidget *parent) + : QStatusBar(parent), + excellentPixmap(QIcon(":/res/images/network-excellent.svg").pixmap(QSize(16, 16))), + goodPixmap(QIcon(":/res/images/network-good.svg").pixmap(QSize(16, 16))), + mediocrePixmap(QIcon(":/res/images/network-mediocre.svg").pixmap(QSize(16, 16))), + badPixmap(QIcon(":/res/images/network-bad.svg").pixmap(QSize(16, 16))), + recordingActivePixmap(QIcon(":/res/images/recording-active.svg").pixmap(QSize(16, 16))), + recordingPausePixmap(QIcon(":/res/images/recording-pause.svg").pixmap(QSize(16, 16))), + streamingActivePixmap(QIcon(":/res/images/streaming-active.svg").pixmap(QSize(16, 16))) +{ + congestionArray.reserve(congestionUpdateSeconds); + + statusWidget = new StatusBarWidget(this); + statusWidget->ui->delayInfo->setText(""); + statusWidget->ui->droppedFrames->setText(QTStr("DroppedFrames").arg("0", "0.0")); + statusWidget->ui->statusIcon->setPixmap(inactivePixmap); + statusWidget->ui->streamIcon->setPixmap(streamingInactivePixmap); + statusWidget->ui->streamTime->setDisabled(true); + statusWidget->ui->recordIcon->setPixmap(recordingInactivePixmap); + statusWidget->ui->recordTime->setDisabled(true); + statusWidget->ui->delayFrame->hide(); + statusWidget->ui->issuesFrame->hide(); + statusWidget->ui->kbps->hide(); + + addPermanentWidget(statusWidget, 1); + setMinimumHeight(statusWidget->height()); + + UpdateIcons(); + connect(App(), &OBSApp::StyleChanged, this, &OBSBasicStatusBar::UpdateIcons); + + messageTimer = new QTimer(this); + messageTimer->setSingleShot(true); + connect(messageTimer, &QTimer::timeout, this, &OBSBasicStatusBar::clearMessage); + + clearMessage(); +} + +void OBSBasicStatusBar::Activate() +{ + if (!active) { + refreshTimer = new QTimer(this); + connect(refreshTimer, &QTimer::timeout, this, &OBSBasicStatusBar::UpdateStatusBar); + + int skipped = video_output_get_skipped_frames(obs_get_video()); + int total = video_output_get_total_frames(obs_get_video()); + + totalStreamSeconds = 0; + totalRecordSeconds = 0; + lastSkippedFrameCount = 0; + startSkippedFrameCount = skipped; + startTotalFrameCount = total; + + refreshTimer->start(1000); + active = true; + + if (streamOutput) { + statusWidget->ui->statusIcon->setPixmap(inactivePixmap); + } + } + + if (streamOutput) { + statusWidget->ui->streamIcon->setPixmap(streamingActivePixmap); + statusWidget->ui->streamTime->setDisabled(false); + statusWidget->ui->issuesFrame->show(); + statusWidget->ui->kbps->show(); + firstCongestionUpdate = true; + } + + if (recordOutput) { + statusWidget->ui->recordIcon->setPixmap(recordingActivePixmap); + statusWidget->ui->recordTime->setDisabled(false); + } +} + +void OBSBasicStatusBar::Deactivate() +{ + OBSBasic *main = qobject_cast(parent()); + if (!main) + return; + + if (!streamOutput) { + statusWidget->ui->streamTime->setText(QString("00:00:00")); + statusWidget->ui->streamTime->setDisabled(true); + statusWidget->ui->streamIcon->setPixmap(streamingInactivePixmap); + statusWidget->ui->statusIcon->setPixmap(inactivePixmap); + statusWidget->ui->delayFrame->hide(); + statusWidget->ui->issuesFrame->hide(); + statusWidget->ui->kbps->hide(); + totalStreamSeconds = 0; + congestionArray.clear(); + disconnected = false; + firstCongestionUpdate = false; + } + + if (!recordOutput) { + statusWidget->ui->recordTime->setText(QString("00:00:00")); + statusWidget->ui->recordTime->setDisabled(true); + statusWidget->ui->recordIcon->setPixmap(recordingInactivePixmap); + totalRecordSeconds = 0; + } + + if (main->outputHandler && !main->outputHandler->Active()) { + delete refreshTimer; + + statusWidget->ui->delayInfo->setText(""); + statusWidget->ui->droppedFrames->setText(QTStr("DroppedFrames").arg("0", "0.0")); + statusWidget->ui->kbps->setText("0 kbps"); + + delaySecTotal = 0; + delaySecStarting = 0; + delaySecStopping = 0; + reconnectTimeout = 0; + active = false; + overloadedNotify = true; + + statusWidget->ui->statusIcon->setPixmap(inactivePixmap); + } +} + +void OBSBasicStatusBar::UpdateDelayMsg() +{ + QString msg; + + if (delaySecTotal) { + if (delaySecStarting && !delaySecStopping) { + msg = QTStr("Basic.StatusBar.DelayStartingIn"); + msg = msg.arg(QString::number(delaySecStarting)); + + } else if (!delaySecStarting && delaySecStopping) { + msg = QTStr("Basic.StatusBar.DelayStoppingIn"); + msg = msg.arg(QString::number(delaySecStopping)); + + } else if (delaySecStarting && delaySecStopping) { + msg = QTStr("Basic.StatusBar.DelayStartingStoppingIn"); + msg = msg.arg(QString::number(delaySecStopping), QString::number(delaySecStarting)); + } else { + msg = QTStr("Basic.StatusBar.Delay"); + msg = msg.arg(QString::number(delaySecTotal)); + } + + if (!statusWidget->ui->delayFrame->isVisible()) + statusWidget->ui->delayFrame->show(); + + statusWidget->ui->delayInfo->setText(msg); + } +} + +void OBSBasicStatusBar::UpdateBandwidth() +{ + if (!streamOutput) + return; + + if (++seconds < bitrateUpdateSeconds) + return; + + OBSOutput output = OBSGetStrongRef(streamOutput); + if (!output) + return; + + uint64_t bytesSent = obs_output_get_total_bytes(output); + uint64_t bytesSentTime = os_gettime_ns(); + + if (bytesSent < lastBytesSent) + bytesSent = 0; + if (bytesSent == 0) + lastBytesSent = 0; + + uint64_t bitsBetween = (bytesSent - lastBytesSent) * 8; + + double timePassed = double(bytesSentTime - lastBytesSentTime) / 1000000000.0; + + double kbitsPerSec = double(bitsBetween) / timePassed / 1000.0; + + QString text; + text += QString::number(kbitsPerSec, 'f', 0) + QString(" kbps"); + + statusWidget->ui->kbps->setText(text); + statusWidget->ui->kbps->setMinimumWidth(statusWidget->ui->kbps->width()); + + if (!statusWidget->ui->kbps->isVisible()) + statusWidget->ui->kbps->show(); + + lastBytesSent = bytesSent; + lastBytesSentTime = bytesSentTime; + seconds = 0; +} + +void OBSBasicStatusBar::UpdateCPUUsage() +{ + OBSBasic *main = qobject_cast(parent()); + if (!main) + return; + + QString text; + text += QString("CPU: ") + QString::number(main->GetCPUUsage(), 'f', 1) + QString("%"); + + statusWidget->ui->cpuUsage->setText(text); + statusWidget->ui->cpuUsage->setMinimumWidth(statusWidget->ui->cpuUsage->width()); + + UpdateCurrentFPS(); +} + +void OBSBasicStatusBar::UpdateCurrentFPS() +{ + struct obs_video_info ovi; + obs_get_video_info(&ovi); + float targetFPS = (float)ovi.fps_num / (float)ovi.fps_den; + + QString text = QString::asprintf("%.2f / %.2f FPS", obs_get_active_fps(), targetFPS); + + statusWidget->ui->fpsCurrent->setText(text); + statusWidget->ui->fpsCurrent->setMinimumWidth(statusWidget->ui->fpsCurrent->width()); +} + +void OBSBasicStatusBar::UpdateStreamTime() +{ + totalStreamSeconds++; + + int seconds = totalStreamSeconds % 60; + int totalMinutes = totalStreamSeconds / 60; + int minutes = totalMinutes % 60; + int hours = totalMinutes / 60; + + QString text = QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds); + statusWidget->ui->streamTime->setText(text); + if (streamOutput && !statusWidget->ui->streamTime->isEnabled()) + statusWidget->ui->streamTime->setDisabled(false); + + if (reconnectTimeout > 0) { + QString msg = QTStr("Basic.StatusBar.Reconnecting") + .arg(QString::number(retries), QString::number(reconnectTimeout)); + showMessage(msg); + disconnected = true; + statusWidget->ui->statusIcon->setPixmap(disconnectedPixmap); + congestionArray.clear(); + reconnectTimeout--; + + } else if (retries > 0) { + QString msg = QTStr("Basic.StatusBar.AttemptingReconnect"); + showMessage(msg.arg(QString::number(retries))); + } + + if (delaySecStopping > 0 || delaySecStarting > 0) { + if (delaySecStopping > 0) + --delaySecStopping; + if (delaySecStarting > 0) + --delaySecStarting; + UpdateDelayMsg(); + } +} + +extern volatile bool recording_paused; + +void OBSBasicStatusBar::UpdateRecordTime() +{ + bool paused = os_atomic_load_bool(&recording_paused); + + if (!paused) { + totalRecordSeconds++; + + if (recordOutput && !statusWidget->ui->recordTime->isEnabled()) + statusWidget->ui->recordTime->setDisabled(false); + } else { + statusWidget->ui->recordIcon->setPixmap(streamPauseIconToggle ? recordingPauseInactivePixmap + : recordingPausePixmap); + + streamPauseIconToggle = !streamPauseIconToggle; + } + + UpdateRecordTimeLabel(); +} + +void OBSBasicStatusBar::UpdateRecordTimeLabel() +{ + int seconds = totalRecordSeconds % 60; + int totalMinutes = totalRecordSeconds / 60; + int minutes = totalMinutes % 60; + int hours = totalMinutes / 60; + + QString text = QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds); + if (os_atomic_load_bool(&recording_paused)) { + text += QStringLiteral(" (PAUSED)"); + } + + statusWidget->ui->recordTime->setText(text); +} + +void OBSBasicStatusBar::UpdateDroppedFrames() +{ + if (!streamOutput) + return; + + OBSOutput output = OBSGetStrongRef(streamOutput); + if (!output) + return; + + int totalDropped = obs_output_get_frames_dropped(output); + int totalFrames = obs_output_get_total_frames(output); + double percent = (double)totalDropped / (double)totalFrames * 100.0; + + if (!totalFrames) + return; + + QString text = QTStr("DroppedFrames"); + text = text.arg(QString::number(totalDropped), QString::number(percent, 'f', 1)); + statusWidget->ui->droppedFrames->setText(text); + + if (!statusWidget->ui->issuesFrame->isVisible()) + statusWidget->ui->issuesFrame->show(); + + /* ----------------------------------- * + * calculate congestion color */ + + float congestion = obs_output_get_congestion(output); + float avgCongestion = (congestion + lastCongestion) * 0.5f; + if (avgCongestion < congestion) + avgCongestion = congestion; + if (avgCongestion > 1.0f) + avgCongestion = 1.0f; + + lastCongestion = congestion; + + if (disconnected) + return; + + bool update = firstCongestionUpdate; + float congestionOverTime = avgCongestion; + + if (congestionArray.size() >= congestionUpdateSeconds) { + congestionOverTime = accumulate(congestionArray.begin(), congestionArray.end(), 0.0f) / + (float)congestionArray.size(); + congestionArray.clear(); + update = true; + } else { + congestionArray.emplace_back(avgCongestion); + } + + if (update) { + if (congestionOverTime <= excellentThreshold + EPSILON) + statusWidget->ui->statusIcon->setPixmap(excellentPixmap); + else if (congestionOverTime <= goodThreshold) + statusWidget->ui->statusIcon->setPixmap(goodPixmap); + else if (congestionOverTime <= mediocreThreshold) + statusWidget->ui->statusIcon->setPixmap(mediocrePixmap); + else if (congestionOverTime <= badThreshold) + statusWidget->ui->statusIcon->setPixmap(badPixmap); + + firstCongestionUpdate = false; + } +} + +void OBSBasicStatusBar::OBSOutputReconnect(void *data, calldata_t *params) +{ + OBSBasicStatusBar *statusBar = reinterpret_cast(data); + + int seconds = (int)calldata_int(params, "timeout_sec"); + QMetaObject::invokeMethod(statusBar, "Reconnect", Q_ARG(int, seconds)); +} + +void OBSBasicStatusBar::OBSOutputReconnectSuccess(void *data, calldata_t *) +{ + OBSBasicStatusBar *statusBar = reinterpret_cast(data); + + QMetaObject::invokeMethod(statusBar, "ReconnectSuccess"); +} + +void OBSBasicStatusBar::Reconnect(int seconds) +{ + OBSBasic *main = qobject_cast(parent()); + + if (!retries) + main->SysTrayNotify(QTStr("Basic.SystemTray.Message.Reconnecting"), QSystemTrayIcon::Warning); + + reconnectTimeout = seconds; + + if (streamOutput) { + OBSOutput output = OBSGetStrongRef(streamOutput); + if (!output) + return; + + delaySecTotal = obs_output_get_active_delay(output); + UpdateDelayMsg(); + + retries++; + } +} + +void OBSBasicStatusBar::ReconnectClear() +{ + retries = 0; + reconnectTimeout = 0; + seconds = -1; + lastBytesSent = 0; + lastBytesSentTime = os_gettime_ns(); + delaySecTotal = 0; + UpdateDelayMsg(); +} + +void OBSBasicStatusBar::ReconnectSuccess() +{ + OBSBasic *main = qobject_cast(parent()); + + QString msg = QTStr("Basic.StatusBar.ReconnectSuccessful"); + showMessage(msg, 4000); + main->SysTrayNotify(msg, QSystemTrayIcon::Information); + ReconnectClear(); + + if (streamOutput) { + OBSOutput output = OBSGetStrongRef(streamOutput); + if (!output) + return; + + delaySecTotal = obs_output_get_active_delay(output); + UpdateDelayMsg(); + disconnected = false; + firstCongestionUpdate = true; + } +} + +void OBSBasicStatusBar::UpdateStatusBar() +{ + OBSBasic *main = qobject_cast(parent()); + + UpdateBandwidth(); + + if (streamOutput) + UpdateStreamTime(); + + if (recordOutput) + UpdateRecordTime(); + + UpdateDroppedFrames(); + + int skipped = video_output_get_skipped_frames(obs_get_video()); + int total = video_output_get_total_frames(obs_get_video()); + + skipped -= startSkippedFrameCount; + total -= startTotalFrameCount; + + int diff = skipped - lastSkippedFrameCount; + double percentage = double(skipped) / double(total) * 100.0; + + if (diff > 10 && percentage >= 0.1f) { + showMessage(QTStr("HighResourceUsage"), 4000); + if (!main->isVisible() && overloadedNotify) { + main->SysTrayNotify(QTStr("HighResourceUsage"), QSystemTrayIcon::Warning); + overloadedNotify = false; + } + } + + lastSkippedFrameCount = skipped; +} + +void OBSBasicStatusBar::StreamDelayStarting(int sec) +{ + OBSBasic *main = qobject_cast(parent()); + if (!main || !main->outputHandler) + return; + + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + streamOutput = OBSGetWeakRef(output); + + delaySecTotal = delaySecStarting = sec; + UpdateDelayMsg(); + Activate(); +} + +void OBSBasicStatusBar::StreamDelayStopping(int sec) +{ + delaySecTotal = delaySecStopping = sec; + UpdateDelayMsg(); +} + +void OBSBasicStatusBar::StreamStarted(obs_output_t *output) +{ + streamOutput = OBSGetWeakRef(output); + + streamSigs.emplace_back(obs_output_get_signal_handler(output), "reconnect", OBSOutputReconnect, this); + streamSigs.emplace_back(obs_output_get_signal_handler(output), "reconnect_success", OBSOutputReconnectSuccess, + this); + + retries = 0; + lastBytesSent = 0; + lastBytesSentTime = os_gettime_ns(); + Activate(); +} + +void OBSBasicStatusBar::StreamStopped() +{ + if (streamOutput) { + streamSigs.clear(); + + ReconnectClear(); + streamOutput = nullptr; + clearMessage(); + Deactivate(); + } +} + +void OBSBasicStatusBar::RecordingStarted(obs_output_t *output) +{ + recordOutput = OBSGetWeakRef(output); + Activate(); +} + +void OBSBasicStatusBar::RecordingStopped() +{ + recordOutput = nullptr; + Deactivate(); +} + +void OBSBasicStatusBar::RecordingPaused() +{ + if (recordOutput) { + statusWidget->ui->recordIcon->setPixmap(recordingPausePixmap); + streamPauseIconToggle = true; + } + + UpdateRecordTimeLabel(); +} + +void OBSBasicStatusBar::RecordingUnpaused() +{ + if (recordOutput) { + statusWidget->ui->recordIcon->setPixmap(recordingActivePixmap); + } + + UpdateRecordTimeLabel(); +} + +static QPixmap GetPixmap(const QString &filename) +{ + QString path = obs_frontend_is_theme_dark() ? "theme:Dark/" : ":/res/images/"; + return QIcon(path + filename).pixmap(QSize(16, 16)); +} + +void OBSBasicStatusBar::UpdateIcons() +{ + disconnectedPixmap = GetPixmap("network-disconnected.svg"); + inactivePixmap = GetPixmap("network-inactive.svg"); + + streamingInactivePixmap = GetPixmap("streaming-inactive.svg"); + + recordingInactivePixmap = GetPixmap("recording-inactive.svg"); + recordingPauseInactivePixmap = GetPixmap("recording-pause-inactive.svg"); + + bool streaming = obs_frontend_streaming_active(); + + if (!streaming) { + statusWidget->ui->streamIcon->setPixmap(streamingInactivePixmap); + statusWidget->ui->statusIcon->setPixmap(inactivePixmap); + } else { + if (disconnected) + statusWidget->ui->statusIcon->setPixmap(disconnectedPixmap); + } + + bool recording = obs_frontend_recording_active(); + + if (!recording) + statusWidget->ui->recordIcon->setPixmap(recordingInactivePixmap); +} + +void OBSBasicStatusBar::showMessage(const QString &message, int timeout) +{ + messageTimer->stop(); + + statusWidget->ui->message->setText(message); + + if (timeout) + messageTimer->start(timeout); +} + +void OBSBasicStatusBar::clearMessage() +{ + statusWidget->ui->message->setText(""); +} diff --git a/frontend/widgets/StatusBarWidget.hpp b/frontend/widgets/StatusBarWidget.hpp new file mode 100644 index 000000000..38be9e520 --- /dev/null +++ b/frontend/widgets/StatusBarWidget.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include +#include + +class Ui_StatusBarWidget; + +class StatusBarWidget : public QWidget { + Q_OBJECT + + friend class OBSBasicStatusBar; + +private: + std::unique_ptr ui; + +public: + StatusBarWidget(QWidget *parent = nullptr); + ~StatusBarWidget(); +}; + +class OBSBasicStatusBar : public QStatusBar { + Q_OBJECT + +private: + StatusBarWidget *statusWidget = nullptr; + + OBSWeakOutputAutoRelease streamOutput; + std::vector streamSigs; + OBSWeakOutputAutoRelease recordOutput; + bool active = false; + bool overloadedNotify = true; + bool streamPauseIconToggle = false; + bool disconnected = false; + bool firstCongestionUpdate = false; + + std::vector congestionArray; + + int retries = 0; + int totalStreamSeconds = 0; + int totalRecordSeconds = 0; + + int reconnectTimeout = 0; + + int delaySecTotal = 0; + int delaySecStarting = 0; + int delaySecStopping = 0; + + int startSkippedFrameCount = 0; + int startTotalFrameCount = 0; + int lastSkippedFrameCount = 0; + + int seconds = 0; + uint64_t lastBytesSent = 0; + uint64_t lastBytesSentTime = 0; + + QPixmap excellentPixmap; + QPixmap goodPixmap; + QPixmap mediocrePixmap; + QPixmap badPixmap; + QPixmap disconnectedPixmap; + QPixmap inactivePixmap; + + QPixmap recordingActivePixmap; + QPixmap recordingPausePixmap; + QPixmap recordingPauseInactivePixmap; + QPixmap recordingInactivePixmap; + QPixmap streamingActivePixmap; + QPixmap streamingInactivePixmap; + + float lastCongestion = 0.0f; + + QPointer refreshTimer; + QPointer messageTimer; + + obs_output_t *GetOutput(); + + void Activate(); + void Deactivate(); + + void UpdateDelayMsg(); + void UpdateBandwidth(); + void UpdateStreamTime(); + void UpdateRecordTime(); + void UpdateRecordTimeLabel(); + void UpdateDroppedFrames(); + + static void OBSOutputReconnect(void *data, calldata_t *params); + static void OBSOutputReconnectSuccess(void *data, calldata_t *params); + +public slots: + void UpdateCPUUsage(); + + void clearMessage(); + void showMessage(const QString &message, int timeout = 0); + +private slots: + void Reconnect(int seconds); + void ReconnectSuccess(); + void UpdateStatusBar(); + void UpdateCurrentFPS(); + void UpdateIcons(); + +public: + OBSBasicStatusBar(QWidget *parent); + + void StreamDelayStarting(int sec); + void StreamDelayStopping(int sec); + void StreamStarted(obs_output_t *output); + void StreamStopped(); + void RecordingStarted(obs_output_t *output); + void RecordingStopped(); + void RecordingPaused(); + void RecordingUnpaused(); + + void ReconnectClear(); +}; diff --git a/frontend/widgets/VolControl.cpp b/frontend/widgets/VolControl.cpp new file mode 100644 index 000000000..cd00c14d7 --- /dev/null +++ b/frontend/widgets/VolControl.cpp @@ -0,0 +1,1507 @@ +#include "window-basic-main.hpp" +#include "moc_volume-control.cpp" +#include "obs-app.hpp" +#include "mute-checkbox.hpp" +#include "absolute-slider.hpp" +#include "source-label.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +#define FADER_PRECISION 4096.0 + +// Size of the audio indicator in pixels +#define INDICATOR_THICKNESS 3 + +// Padding on top and bottom of vertical meters +#define METER_PADDING 1 + +std::weak_ptr VolumeMeter::updateTimer; + +static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) +{ + if (muted) + return Qt::Checked; + else if (unassigned) + return Qt::PartiallyChecked; + else + return Qt::Unchecked; +} + +static inline bool IsSourceUnassigned(obs_source_t *source) +{ + uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); + obs_monitoring_type mt = obs_source_get_monitoring_type(source); + + return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; +} + +static void ShowUnassignedWarning(const char *name) +{ + auto msgBox = [=]() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); + msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); +} + +void VolControl::OBSVolumeChanged(void *data, float db) +{ + Q_UNUSED(db); + VolControl *volControl = static_cast(data); + + QMetaObject::invokeMethod(volControl, "VolumeChanged"); +} + +void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + VolControl *volControl = static_cast(data); + + volControl->volMeter->setLevels(magnitude, peak, inputPeak); +} + +void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) +{ + VolControl *volControl = static_cast(data); + bool muted = calldata_bool(calldata, "muted"); + + QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); +} + +void VolControl::VolumeChanged() +{ + slider->blockSignals(true); + slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); + slider->blockSignals(false); + + updateText(); +} + +void VolControl::VolumeMuted(bool muted) +{ + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) +{ + + VolControl *volControl = static_cast(data); + QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); +} + +void VolControl::MixersOrMonitoringChanged() +{ + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::SetMuted(bool) +{ + bool checked = mute->checkState() == Qt::Checked; + bool prev = obs_source_muted(source); + obs_source_set_muted(source, checked); + bool unassigned = IsSourceUnassigned(source); + + if (!checked && unassigned) { + mute->setCheckState(Qt::PartiallyChecked); + /* Show notice about the source no being assigned to any tracks */ + bool has_shown_warning = + config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); + if (!has_shown_warning) + ShowUnassignedWarning(obs_source_get_name(source)); + } + + auto undo_redo = [](const std::string &uuid, bool val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_muted(source, val); + }; + + QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); + + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); +} + +void VolControl::SliderChanged(int vol) +{ + float prev = obs_source_get_volume(source); + + obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); + updateText(); + + auto undo_redo = [](const std::string &uuid, float val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_volume(source, val); + }; + + float val = obs_source_get_volume(source); + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), + std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); +} + +void VolControl::updateText() +{ + QString text; + float db = obs_fader_get_db(obs_fader); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + volLabel->setText(text); + + bool muted = obs_source_muted(source); + const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; + + QString sourceName = obs_source_get_name(source); + QString accText = QTStr(accTextLookup).arg(sourceName); + + slider->setAccessibleName(accText); +} + +void VolControl::EmitConfigClicked() +{ + emit ConfigClicked(); +} + +void VolControl::SetMeterDecayRate(qreal q) +{ + volMeter->setPeakDecayRate(q); +} + +void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + volMeter->setPeakMeterType(peakMeterType); +} + +VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) + : source(std::move(source_)), + levelTotal(0.0f), + levelCount(0.0f), + obs_fader(obs_fader_create(OBS_FADER_LOG)), + obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), + vertical(vertical), + contextMenu(nullptr) +{ + nameLabel = new OBSSourceLabel(source); + volLabel = new QLabel(); + mute = new MuteCheckBox(); + + volLabel->setObjectName("volLabel"); + volLabel->setAlignment(Qt::AlignCenter); + +#ifdef __APPLE__ + mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + QString sourceName = obs_source_get_name(source); + setObjectName(sourceName); + + if (showConfig) { + config = new QPushButton(this); + config->setProperty("class", "icon-dots-vert"); + config->setAutoDefault(false); + + config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); + + connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); + } + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + if (vertical) { + QHBoxLayout *nameLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QHBoxLayout *volLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QHBoxLayout *meterLayout = new QHBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, true); + slider = new VolumeSlider(obs_fader, Qt::Vertical); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + nameLayout->setAlignment(Qt::AlignCenter); + meterLayout->setAlignment(Qt::AlignCenter); + controlLayout->setAlignment(Qt::AlignCenter); + volLayout->setAlignment(Qt::AlignCenter); + + meterFrame->setObjectName("volMeterFrame"); + + nameLayout->setContentsMargins(0, 0, 0, 0); + nameLayout->setSpacing(0); + nameLayout->addWidget(nameLabel); + + controlLayout->setContentsMargins(0, 0, 0, 0); + controlLayout->setSpacing(0); + + // Add Headphone (audio monitoring) widget here + controlLayout->addWidget(mute); + + if (showConfig) { + controlLayout->addWidget(config); + } + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + meterLayout->addWidget(slider); + meterLayout->addWidget(volMeter); + + meterFrame->setLayout(meterLayout); + + volLayout->setContentsMargins(0, 0, 0, 0); + volLayout->setSpacing(0); + volLayout->addWidget(volLabel); + volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); + + mainLayout->addItem(nameLayout); + mainLayout->addItem(volLayout); + mainLayout->addWidget(meterFrame); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + + // Default size can cause clipping of long names in vertical layout. + QFont font = nameLabel->font(); + QFontInfo info(font); + nameLabel->setFont(font); + + setMaximumWidth(110); + } else { + QHBoxLayout *textLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QVBoxLayout *meterLayout = new QVBoxLayout; + QVBoxLayout *buttonLayout = new QVBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, false); + volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + + slider = new VolumeSlider(obs_fader, Qt::Horizontal); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + textLayout->setContentsMargins(0, 0, 0, 0); + textLayout->addWidget(nameLabel); + textLayout->addWidget(volLabel); + textLayout->setAlignment(nameLabel, Qt::AlignLeft); + textLayout->setAlignment(volLabel, Qt::AlignRight); + + meterFrame->setObjectName("volMeterFrame"); + meterFrame->setLayout(meterLayout); + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + + meterLayout->addWidget(volMeter); + meterLayout->addWidget(slider); + + buttonLayout->setContentsMargins(0, 0, 0, 0); + buttonLayout->setSpacing(0); + + if (showConfig) { + buttonLayout->addWidget(config); + } + buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); + buttonLayout->addWidget(mute); + + controlLayout->addItem(buttonLayout); + controlLayout->addWidget(meterFrame); + + mainLayout->addItem(textLayout); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + } + + setLayout(mainLayout); + + nameLabel->setText(sourceName); + + slider->setMinimum(0); + slider->setMaximum(int(FADER_PRECISION)); + + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + mute->setCheckState(GetCheckState(muted, unassigned)); + volMeter->muted = muted || unassigned; + mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); + obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, + this); + + QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); + QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); + + obs_fader_attach_source(obs_fader, source); + obs_volmeter_attach_source(obs_volmeter, source); + + /* Call volume changed once to init the slider position and label */ + VolumeChanged(); +} + +void VolControl::EnableSlider(bool enable) +{ + slider->setEnabled(enable); +} + +VolControl::~VolControl() +{ + obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.clear(); + + if (contextMenu) + contextMenu->close(); +} + +static inline QColor color_from_int(long long val) +{ + QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); + color.setAlpha(255); + + return color; +} + +QColor VolumeMeter::getBackgroundNominalColor() const +{ + return p_backgroundNominalColor; +} + +QColor VolumeMeter::getBackgroundNominalColorDisabled() const +{ + return backgroundNominalColorDisabled; +} + +void VolumeMeter::setBackgroundNominalColor(QColor c) +{ + p_backgroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreen")); + } else { + backgroundNominalColor = p_backgroundNominalColor; + } +} + +void VolumeMeter::setBackgroundNominalColorDisabled(QColor c) +{ + backgroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundWarningColor() const +{ + return p_backgroundWarningColor; +} + +QColor VolumeMeter::getBackgroundWarningColorDisabled() const +{ + return backgroundWarningColorDisabled; +} + +void VolumeMeter::setBackgroundWarningColor(QColor c) +{ + p_backgroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellow")); + } else { + backgroundWarningColor = p_backgroundWarningColor; + } +} + +void VolumeMeter::setBackgroundWarningColorDisabled(QColor c) +{ + backgroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundErrorColor() const +{ + return p_backgroundErrorColor; +} + +QColor VolumeMeter::getBackgroundErrorColorDisabled() const +{ + return backgroundErrorColorDisabled; +} + +void VolumeMeter::setBackgroundErrorColor(QColor c) +{ + p_backgroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRed")); + } else { + backgroundErrorColor = p_backgroundErrorColor; + } +} + +void VolumeMeter::setBackgroundErrorColorDisabled(QColor c) +{ + backgroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundNominalColor() const +{ + return p_foregroundNominalColor; +} + +QColor VolumeMeter::getForegroundNominalColorDisabled() const +{ + return foregroundNominalColorDisabled; +} + +void VolumeMeter::setForegroundNominalColor(QColor c) +{ + p_foregroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreenActive")); + } else { + foregroundNominalColor = p_foregroundNominalColor; + } +} + +void VolumeMeter::setForegroundNominalColorDisabled(QColor c) +{ + foregroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundWarningColor() const +{ + return p_foregroundWarningColor; +} + +QColor VolumeMeter::getForegroundWarningColorDisabled() const +{ + return foregroundWarningColorDisabled; +} + +void VolumeMeter::setForegroundWarningColor(QColor c) +{ + p_foregroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellowActive")); + } else { + foregroundWarningColor = p_foregroundWarningColor; + } +} + +void VolumeMeter::setForegroundWarningColorDisabled(QColor c) +{ + foregroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundErrorColor() const +{ + return p_foregroundErrorColor; +} + +QColor VolumeMeter::getForegroundErrorColorDisabled() const +{ + return foregroundErrorColorDisabled; +} + +void VolumeMeter::setForegroundErrorColor(QColor c) +{ + p_foregroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRedActive")); + } else { + foregroundErrorColor = p_foregroundErrorColor; + } +} + +void VolumeMeter::setForegroundErrorColorDisabled(QColor c) +{ + foregroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getClipColor() const +{ + return clipColor; +} + +void VolumeMeter::setClipColor(QColor c) +{ + clipColor = std::move(c); +} + +QColor VolumeMeter::getMagnitudeColor() const +{ + return magnitudeColor; +} + +void VolumeMeter::setMagnitudeColor(QColor c) +{ + magnitudeColor = std::move(c); +} + +QColor VolumeMeter::getMajorTickColor() const +{ + return majorTickColor; +} + +void VolumeMeter::setMajorTickColor(QColor c) +{ + majorTickColor = std::move(c); +} + +QColor VolumeMeter::getMinorTickColor() const +{ + return minorTickColor; +} + +void VolumeMeter::setMinorTickColor(QColor c) +{ + minorTickColor = std::move(c); +} + +int VolumeMeter::getMeterThickness() const +{ + return meterThickness; +} + +void VolumeMeter::setMeterThickness(int v) +{ + meterThickness = v; + recalculateLayout = true; +} + +qreal VolumeMeter::getMeterFontScaling() const +{ + return meterFontScaling; +} + +void VolumeMeter::setMeterFontScaling(qreal v) +{ + meterFontScaling = v; + recalculateLayout = true; +} + +void VolControl::refreshColors() +{ + volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); + volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); + volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); + volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); + volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); + volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); +} + +qreal VolumeMeter::getMinimumLevel() const +{ + return minimumLevel; +} + +void VolumeMeter::setMinimumLevel(qreal v) +{ + minimumLevel = v; +} + +qreal VolumeMeter::getWarningLevel() const +{ + return warningLevel; +} + +void VolumeMeter::setWarningLevel(qreal v) +{ + warningLevel = v; +} + +qreal VolumeMeter::getErrorLevel() const +{ + return errorLevel; +} + +void VolumeMeter::setErrorLevel(qreal v) +{ + errorLevel = v; +} + +qreal VolumeMeter::getClipLevel() const +{ + return clipLevel; +} + +void VolumeMeter::setClipLevel(qreal v) +{ + clipLevel = v; +} + +qreal VolumeMeter::getMinimumInputLevel() const +{ + return minimumInputLevel; +} + +void VolumeMeter::setMinimumInputLevel(qreal v) +{ + minimumInputLevel = v; +} + +qreal VolumeMeter::getPeakDecayRate() const +{ + return peakDecayRate; +} + +void VolumeMeter::setPeakDecayRate(qreal v) +{ + peakDecayRate = v; +} + +qreal VolumeMeter::getMagnitudeIntegrationTime() const +{ + return magnitudeIntegrationTime; +} + +void VolumeMeter::setMagnitudeIntegrationTime(qreal v) +{ + magnitudeIntegrationTime = v; +} + +qreal VolumeMeter::getPeakHoldDuration() const +{ + return peakHoldDuration; +} + +void VolumeMeter::setPeakHoldDuration(qreal v) +{ + peakHoldDuration = v; +} + +qreal VolumeMeter::getInputPeakHoldDuration() const +{ + return inputPeakHoldDuration; +} + +void VolumeMeter::setInputPeakHoldDuration(qreal v) +{ + inputPeakHoldDuration = v; +} + +void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); + switch (peakMeterType) { + case TRUE_PEAK_METER: + // For true-peak meters EBU has defined the Permitted Maximum, + // taking into account the accuracy of the meter and further + // processing required by lossy audio compression. + // + // The alignment level was not specified, but I've adjusted + // it compared to a sample-peak meter. Incidentally Youtube + // uses this new Alignment Level as the maximum integrated + // loudness of a video. + // + // * Permitted Maximum Level (PML) = -2.0 dBTP + // * Alignment Level (AL) = -13 dBTP + setErrorLevel(-2.0); + setWarningLevel(-13.0); + break; + + case SAMPLE_PEAK_METER: + default: + // For a sample Peak Meter EBU has the following level + // definitions, taking into account inaccuracies of this meter: + // + // * Permitted Maximum Level (PML) = -9.0 dBFS + // * Alignment Level (AL) = -20.0 dBFS + setErrorLevel(-9.0); + setWarningLevel(-20.0); + break; + } +} + +void VolumeMeter::mousePressEvent(QMouseEvent *event) +{ + setFocus(Qt::MouseFocusReason); + event->accept(); +} + +void VolumeMeter::wheelEvent(QWheelEvent *event) +{ + QApplication::sendEvent(focusProxy(), event); +} + +VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, bool vertical) + : QWidget(parent), + obs_volmeter(obs_volmeter), + vertical(vertical) +{ + setAttribute(Qt::WA_OpaquePaintEvent, true); + + // Default meter settings, they only show if + // there is no stylesheet, do not remove. + backgroundNominalColor.setRgb(0x26, 0x7f, 0x26); // Dark green + backgroundWarningColor.setRgb(0x7f, 0x7f, 0x26); // Dark yellow + backgroundErrorColor.setRgb(0x7f, 0x26, 0x26); // Dark red + foregroundNominalColor.setRgb(0x4c, 0xff, 0x4c); // Bright green + foregroundWarningColor.setRgb(0xff, 0xff, 0x4c); // Bright yellow + foregroundErrorColor.setRgb(0xff, 0x4c, 0x4c); // Bright red + + backgroundNominalColorDisabled.setRgb(90, 90, 90); + backgroundWarningColorDisabled.setRgb(117, 117, 117); + backgroundErrorColorDisabled.setRgb(65, 65, 65); + foregroundNominalColorDisabled.setRgb(163, 163, 163); + foregroundWarningColorDisabled.setRgb(217, 217, 217); + foregroundErrorColorDisabled.setRgb(113, 113, 113); + + clipColor.setRgb(0xff, 0xff, 0xff); // Bright white + magnitudeColor.setRgb(0x00, 0x00, 0x00); // Black + majorTickColor.setRgb(0x00, 0x00, 0x00); // Black + minorTickColor.setRgb(0x32, 0x32, 0x32); // Dark gray + minimumLevel = -60.0; // -60 dB + warningLevel = -20.0; // -20 dB + errorLevel = -9.0; // -9 dB + clipLevel = -0.5; // -0.5 dB + minimumInputLevel = -50.0; // -50 dB + peakDecayRate = 11.76; // 20 dB / 1.7 sec + magnitudeIntegrationTime = 0.3; // 99% in 300 ms + peakHoldDuration = 20.0; // 20 seconds + inputPeakHoldDuration = 1.0; // 1 second + meterThickness = 3; // Bar thickness in pixels + meterFontScaling = 0.7; // Font size for numbers is 70% of Widget's font size + channels = (int)audio_output_get_channels(obs_get_audio()); + + doLayout(); + updateTimerRef = updateTimer.lock(); + if (!updateTimerRef) { + updateTimerRef = std::make_shared(); + updateTimerRef->setTimerType(Qt::PreciseTimer); + updateTimerRef->start(16); + updateTimer = updateTimerRef; + } + + updateTimerRef->AddVolControl(this); +} + +VolumeMeter::~VolumeMeter() +{ + updateTimerRef->RemoveVolControl(this); +} + +void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + uint64_t ts = os_gettime_ns(); + QMutexLocker locker(&dataMutex); + + currentLastUpdateTime = ts; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = magnitude[channelNr]; + currentPeak[channelNr] = peak[channelNr]; + currentInputPeak[channelNr] = inputPeak[channelNr]; + } + + // In case there are more updates then redraws we must make sure + // that the ballistics of peak and hold are recalculated. + locker.unlock(); + calculateBallistics(ts); +} + +inline void VolumeMeter::resetLevels() +{ + currentLastUpdateTime = 0; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = -M_INFINITE; + currentPeak[channelNr] = -M_INFINITE; + currentInputPeak[channelNr] = -M_INFINITE; + + displayMagnitude[channelNr] = -M_INFINITE; + displayPeak[channelNr] = -M_INFINITE; + displayPeakHold[channelNr] = -M_INFINITE; + displayPeakHoldLastUpdateTime[channelNr] = 0; + displayInputPeakHold[channelNr] = -M_INFINITE; + displayInputPeakHoldLastUpdateTime[channelNr] = 0; + } +} + +bool VolumeMeter::needLayoutChange() +{ + int currentNrAudioChannels = obs_volmeter_get_nr_channels(obs_volmeter); + + if (!currentNrAudioChannels) { + struct obs_audio_info oai; + obs_get_audio_info(&oai); + currentNrAudioChannels = (oai.speakers == SPEAKERS_MONO) ? 1 : 2; + } + + if (displayNrAudioChannels != currentNrAudioChannels) { + displayNrAudioChannels = currentNrAudioChannels; + recalculateLayout = true; + } + + return recalculateLayout; +} + +// When this is called from the constructor, obs_volmeter_get_nr_channels has not +// yet been called and Q_PROPERTY settings have not yet been read from the +// stylesheet. +inline void VolumeMeter::doLayout() +{ + QMutexLocker locker(&dataMutex); + + if (displayNrAudioChannels) { + int meterSize = std::floor(22 / displayNrAudioChannels); + setMeterThickness(std::clamp(meterSize, 3, 7)); + } + recalculateLayout = false; + + tickFont = font(); + QFontInfo info(tickFont); + tickFont.setPointSizeF(info.pointSizeF() * meterFontScaling); + QFontMetrics metrics(tickFont); + if (vertical) { + // Each meter channel is meterThickness pixels wide, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, space to hold our longest label in this font, + // and a few pixels before the fader. + QRect scaleBounds = metrics.boundingRect("-88"); + setMinimumSize(displayNrAudioChannels * (meterThickness + 1) - 1 + 10 + scaleBounds.width() + 2, 100); + } else { + // Each meter channel is meterThickness pixels high, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, and space high enough to hold our label in + // this font, presuming that digits don't have descenders. + setMinimumSize(100, displayNrAudioChannels * (meterThickness + 1) - 1 + 4 + metrics.capHeight()); + } + + resetLevels(); +} + +inline bool VolumeMeter::detectIdle(uint64_t ts) +{ + double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; + if (timeSinceLastUpdate > 0.5) { + resetLevels(); + return true; + } else { + return false; + } +} + +inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw) +{ + if (currentPeak[channelNr] >= displayPeak[channelNr] || isnan(displayPeak[channelNr])) { + // Attack of peak is immediate. + displayPeak[channelNr] = currentPeak[channelNr]; + } else { + // Decay of peak is 40 dB / 1.7 seconds for Fast Profile + // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) + // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) + float decay = float(peakDecayRate * timeSinceLastRedraw); + displayPeak[channelNr] = + std::clamp(displayPeak[channelNr] - decay, std::min(currentPeak[channelNr], 0.f), 0.f); + } + + if (currentPeak[channelNr] >= displayPeakHold[channelNr] || !isfinite(displayPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak + // after 20 seconds. + qreal timeSinceLastPeak = (uint64_t)(ts - displayPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > peakHoldDuration) { + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (currentInputPeak[channelNr] >= displayInputPeakHold[channelNr] || + !isfinite(displayInputPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak after 1 second. + qreal timeSinceLastPeak = (uint64_t)(ts - displayInputPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > inputPeakHoldDuration) { + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (!isfinite(displayMagnitude[channelNr])) { + // The statements in the else-leg do not work with + // NaN and infinite displayMagnitude. + displayMagnitude[channelNr] = currentMagnitude[channelNr]; + } else { + // A VU meter will integrate to the new value to 99% in 300 ms. + // The calculation here is very simplified and is more accurate + // with higher frame-rate. + float attack = float((currentMagnitude[channelNr] - displayMagnitude[channelNr]) * + (timeSinceLastRedraw / magnitudeIntegrationTime) * 0.99); + displayMagnitude[channelNr] = + std::clamp(displayMagnitude[channelNr] + attack, (float)minimumLevel, 0.f); + } +} + +inline void VolumeMeter::calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw) +{ + QMutexLocker locker(&dataMutex); + + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) + calculateBallisticsForChannel(channelNr, ts, timeSinceLastRedraw); +} + +void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold) +{ + QMutexLocker locker(&dataMutex); + QColor color; + + if (peakHold < minimumInputLevel) + color = backgroundNominalColor; + else if (peakHold < warningLevel) + color = foregroundNominalColor; + else if (peakHold < errorLevel) + color = foregroundWarningColor; + else if (peakHold <= clipLevel) + color = foregroundErrorColor; + else + color = clipColor; + + painter.fillRect(x, y, width, height, color); +} + +void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width) +{ + qreal scale = width / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = int(x + width - (i * scale) - 1); + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + QRect textBounds = metrics.boundingRect(str); + int pos; + if (i == 0) { + pos = position - textBounds.width(); + } else { + pos = position - (textBounds.width() / 2); + if (pos < 0) + pos = 0; + } + painter.drawText(pos, y + 4 + metrics.capHeight(), str); + + painter.drawLine(position, y, position, y + 2); + } +} + +void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) +{ + qreal scale = height / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = y + int(i * scale) + METER_PADDING; + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + if (i == 0) { + painter.drawText(x + 10, position + metrics.capHeight(), str); + } else { + painter.drawText(x + 8, position + (metrics.capHeight() / 2), str); + } + + painter.drawLine(x, position, x + 2, position); + } +} + +#define CLIP_FLASH_DURATION_MS 1000 + +inline int VolumeMeter::convertToInt(float number) +{ + constexpr int min = std::numeric_limits::min(); + constexpr int max = std::numeric_limits::max(); + + // NOTE: Conversion from 'const int' to 'float' changes max value from 2147483647 to 2147483648 + if (number >= (float)max) + return max; + else if (number < min) + return min; + else + return int(number); +} + +void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = width / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = x + 0; + int maximumPosition = x + width; + int magnitudePosition = x + width - convertToInt(magnitude * scale); + int peakPosition = x + width - convertToInt(peak * scale); + int peakHoldPosition = x + width - convertToInt(peakHold * scale); + int warningPosition = x + width - convertToInt(warningLevel * scale); + int errorPosition = x + width - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(minimumPosition, y, peakPosition - minimumPosition, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(peakPosition, y, warningPosition - peakPosition, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, peakPosition - warningPosition, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(peakPosition, y, errorPosition - peakPosition, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(errorPosition, y, peakPosition - errorPosition, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(peakPosition, y, maximumPosition - peakPosition, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(minimumPosition, y, end, height, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(magnitudePosition - 3, y, 3, height, magnitudeColor); +} + +void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = height / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = y + 0; + int maximumPosition = y + height; + int magnitudePosition = y + height - convertToInt(magnitude * scale); + int peakPosition = y + height - convertToInt(peak * scale); + int peakHoldPosition = y + height - convertToInt(peakHold * scale); + int warningPosition = y + height - convertToInt(warningLevel * scale); + int errorPosition = y + height - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(x, minimumPosition, width, peakPosition - minimumPosition, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, peakPosition, width, warningPosition - peakPosition, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, peakPosition - warningPosition, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, peakPosition, width, errorPosition - peakPosition, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, errorPosition, width, peakPosition - errorPosition, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(x, peakPosition, width, maximumPosition - peakPosition, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(x, minimumPosition, width, end, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(x, magnitudePosition - 3, width, 3, magnitudeColor); +} + +void VolumeMeter::paintEvent(QPaintEvent *event) +{ + uint64_t ts = os_gettime_ns(); + qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; + calculateBallistics(ts, timeSinceLastRedraw); + bool idle = detectIdle(ts); + + QRect widgetRect = rect(); + int width = widgetRect.width(); + int height = widgetRect.height(); + + QPainter painter(this); + + // Paint window background color (as widget is opaque) + QColor background = palette().color(QPalette::ColorRole::Window); + painter.fillRect(event->region().boundingRect(), background); + + if (vertical) + height -= METER_PADDING * 2; + + // timerEvent requests update of the bar(s) only, so we can avoid the + // overhead of repainting the scale and labels. + if (event->region().boundingRect() != getBarRect()) { + if (needLayoutChange()) + doLayout(); + + if (vertical) { + paintVTicks(painter, displayNrAudioChannels * (meterThickness + 1) - 1, 0, + height - (INDICATOR_THICKNESS + 3)); + } else { + paintHTicks(painter, INDICATOR_THICKNESS + 3, displayNrAudioChannels * (meterThickness + 1) - 1, + width - (INDICATOR_THICKNESS + 3)); + } + } + + if (vertical) { + // Invert the Y axis to ease the math + painter.translate(0, height + METER_PADDING); + painter.scale(1, -1); + } + + for (int channelNr = 0; channelNr < displayNrAudioChannels; channelNr++) { + + int channelNrFixed = (displayNrAudioChannels == 1 && channels > 2) ? 2 : channelNr; + + if (vertical) + paintVMeter(painter, channelNr * (meterThickness + 1), INDICATOR_THICKNESS + 2, meterThickness, + height - (INDICATOR_THICKNESS + 2), displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + else + paintHMeter(painter, INDICATOR_THICKNESS + 2, channelNr * (meterThickness + 1), + width - (INDICATOR_THICKNESS + 2), meterThickness, displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + + if (idle) + continue; + + // By not drawing the input meter boxes the user can + // see that the audio stream has been stopped, without + // having too much visual impact. + if (vertical) + paintInputMeter(painter, channelNr * (meterThickness + 1), 0, meterThickness, + INDICATOR_THICKNESS, displayInputPeakHold[channelNrFixed]); + else + paintInputMeter(painter, 0, channelNr * (meterThickness + 1), INDICATOR_THICKNESS, + meterThickness, displayInputPeakHold[channelNrFixed]); + } + + lastRedrawTime = ts; +} + +QRect VolumeMeter::getBarRect() const +{ + QRect rec = rect(); + if (vertical) + rec.setWidth(displayNrAudioChannels * (meterThickness + 1) - 1); + else + rec.setHeight(displayNrAudioChannels * (meterThickness + 1) - 1); + + return rec; +} + +void VolumeMeter::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::StyleChange) + recalculateLayout = true; + + QWidget::changeEvent(e); +} + +void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) +{ + volumeMeters.push_back(meter); +} + +void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) +{ + volumeMeters.removeOne(meter); +} + +void VolumeMeterTimer::timerEvent(QTimerEvent *) +{ + for (VolumeMeter *meter : volumeMeters) { + if (meter->needLayoutChange()) { + // Tell paintEvent to update layout and paint everything + meter->update(); + } else { + // Tell paintEvent to paint only the bars + meter->update(meter->getBarRect()); + } + } +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) +{ + fad = fader; +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) + : AbsoluteSlider(orientation, parent) +{ + fad = fader; +} + +bool VolumeSlider::getDisplayTicks() const +{ + return displayTicks; +} + +void VolumeSlider::setDisplayTicks(bool display) +{ + displayTicks = display; +} + +void VolumeSlider::paintEvent(QPaintEvent *event) +{ + if (!getDisplayTicks()) { + QSlider::paintEvent(event); + return; + } + + QPainter painter(this); + QColor tickColor(91, 98, 115, 255); + + obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); + + QStyleOptionSlider opt; + initStyleOption(&opt); + + QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); + QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); + + if (orientation() == Qt::Horizontal) { + const int sliderWidth = groove.width() - handle.width(); + + float tickLength = groove.height() * 1.5; + tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); + + float yPos = groove.center().y() - (tickLength / 2) + 1; + + for (int db = -10; db >= -90; db -= 10) { + float tickValue = fader_db_to_def(db); + + float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); + painter.fillRect(xPos, yPos, 1, tickLength, tickColor); + } + } + + if (orientation() == Qt::Vertical) { + const int sliderHeight = groove.height() - handle.height(); + + float tickLength = groove.width() * 1.5; + tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); + + float xPos = groove.center().x() - (tickLength / 2) + 1; + + for (int db = -10; db >= -96; db -= 10) { + float tickValue = fader_db_to_def(db); + + float yPos = + groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); + painter.fillRect(xPos, yPos, tickLength, 1, tickColor); + } + } + + QSlider::paintEvent(event); +} + +VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} + +VolumeSlider *VolumeAccessibleInterface::slider() const +{ + return qobject_cast(object()); +} + +QString VolumeAccessibleInterface::text(QAccessible::Text t) const +{ + if (slider()->isVisible()) { + switch (t) { + case QAccessible::Text::Value: + return currentValue().toString(); + default: + break; + } + } + return QAccessibleWidget::text(t); +} + +QVariant VolumeAccessibleInterface::currentValue() const +{ + QString text; + float db = obs_fader_get_db(slider()->fad); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + return text; +} + +void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) +{ + slider()->setValue(value.toInt()); +} + +QVariant VolumeAccessibleInterface::maximumValue() const +{ + return slider()->maximum(); +} + +QVariant VolumeAccessibleInterface::minimumValue() const +{ + return slider()->minimum(); +} + +QVariant VolumeAccessibleInterface::minimumStepSize() const +{ + return slider()->singleStep(); +} + +QAccessible::Role VolumeAccessibleInterface::role() const +{ + return QAccessible::Role::Slider; +} diff --git a/frontend/widgets/VolControl.hpp b/frontend/widgets/VolControl.hpp new file mode 100644 index 000000000..51c77f247 --- /dev/null +++ b/frontend/widgets/VolControl.hpp @@ -0,0 +1,341 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "absolute-slider.hpp" + +class QPushButton; +class VolumeMeterTimer; +class VolumeSlider; + +class VolumeMeter : public QWidget { + Q_OBJECT + Q_PROPERTY(QColor backgroundNominalColor READ getBackgroundNominalColor WRITE setBackgroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColor READ getBackgroundWarningColor WRITE setBackgroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor backgroundErrorColor READ getBackgroundErrorColor WRITE setBackgroundErrorColor DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColor READ getForegroundNominalColor WRITE setForegroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColor READ getForegroundWarningColor WRITE setForegroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor foregroundErrorColor READ getForegroundErrorColor WRITE setForegroundErrorColor DESIGNABLE true) + + Q_PROPERTY(QColor backgroundNominalColorDisabled READ getBackgroundNominalColorDisabled WRITE + setBackgroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColorDisabled READ getBackgroundWarningColorDisabled WRITE + setBackgroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundErrorColorDisabled READ getBackgroundErrorColorDisabled WRITE + setBackgroundErrorColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColorDisabled READ getForegroundNominalColorDisabled WRITE + setForegroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColorDisabled READ getForegroundWarningColorDisabled WRITE + setForegroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundErrorColorDisabled READ getForegroundErrorColorDisabled WRITE + setForegroundErrorColorDisabled DESIGNABLE true) + + Q_PROPERTY(QColor clipColor READ getClipColor WRITE setClipColor DESIGNABLE true) + Q_PROPERTY(QColor magnitudeColor READ getMagnitudeColor WRITE setMagnitudeColor DESIGNABLE true) + Q_PROPERTY(QColor majorTickColor READ getMajorTickColor WRITE setMajorTickColor DESIGNABLE true) + Q_PROPERTY(QColor minorTickColor READ getMinorTickColor WRITE setMinorTickColor DESIGNABLE true) + Q_PROPERTY(int meterThickness READ getMeterThickness WRITE setMeterThickness DESIGNABLE true) + Q_PROPERTY(qreal meterFontScaling READ getMeterFontScaling WRITE setMeterFontScaling DESIGNABLE true) + + // Levels are denoted in dBFS. + Q_PROPERTY(qreal minimumLevel READ getMinimumLevel WRITE setMinimumLevel DESIGNABLE true) + Q_PROPERTY(qreal warningLevel READ getWarningLevel WRITE setWarningLevel DESIGNABLE true) + Q_PROPERTY(qreal errorLevel READ getErrorLevel WRITE setErrorLevel DESIGNABLE true) + Q_PROPERTY(qreal clipLevel READ getClipLevel WRITE setClipLevel DESIGNABLE true) + Q_PROPERTY(qreal minimumInputLevel READ getMinimumInputLevel WRITE setMinimumInputLevel DESIGNABLE true) + + // Rates are denoted in dB/second. + Q_PROPERTY(qreal peakDecayRate READ getPeakDecayRate WRITE setPeakDecayRate DESIGNABLE true) + + // Time in seconds for the VU meter to integrate over. + Q_PROPERTY(qreal magnitudeIntegrationTime READ getMagnitudeIntegrationTime WRITE setMagnitudeIntegrationTime + DESIGNABLE true) + + // Duration is denoted in seconds. + Q_PROPERTY(qreal peakHoldDuration READ getPeakHoldDuration WRITE setPeakHoldDuration DESIGNABLE true) + Q_PROPERTY(qreal inputPeakHoldDuration READ getInputPeakHoldDuration WRITE setInputPeakHoldDuration + DESIGNABLE true) + + friend class VolControl; + +private: + obs_volmeter_t *obs_volmeter; + static std::weak_ptr updateTimer; + std::shared_ptr updateTimerRef; + + inline void resetLevels(); + inline void doLayout(); + inline bool detectIdle(uint64_t ts); + inline void calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw = 0.0); + inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw); + + inline int convertToInt(float number); + void paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold); + void paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintHTicks(QPainter &painter, int x, int y, int width); + void paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintVTicks(QPainter &painter, int x, int y, int height); + + QMutex dataMutex; + + bool recalculateLayout = true; + uint64_t currentLastUpdateTime = 0; + float currentMagnitude[MAX_AUDIO_CHANNELS]; + float currentPeak[MAX_AUDIO_CHANNELS]; + float currentInputPeak[MAX_AUDIO_CHANNELS]; + + int displayNrAudioChannels = 0; + float displayMagnitude[MAX_AUDIO_CHANNELS]; + float displayPeak[MAX_AUDIO_CHANNELS]; + float displayPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + float displayInputPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayInputPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + + QFont tickFont; + QColor backgroundNominalColor; + QColor backgroundWarningColor; + QColor backgroundErrorColor; + QColor foregroundNominalColor; + QColor foregroundWarningColor; + QColor foregroundErrorColor; + + QColor backgroundNominalColorDisabled; + QColor backgroundWarningColorDisabled; + QColor backgroundErrorColorDisabled; + QColor foregroundNominalColorDisabled; + QColor foregroundWarningColorDisabled; + QColor foregroundErrorColorDisabled; + + QColor clipColor; + QColor magnitudeColor; + QColor majorTickColor; + QColor minorTickColor; + + int meterThickness; + qreal meterFontScaling; + + qreal minimumLevel; + qreal warningLevel; + qreal errorLevel; + qreal clipLevel; + qreal minimumInputLevel; + qreal peakDecayRate; + qreal magnitudeIntegrationTime; + qreal peakHoldDuration; + qreal inputPeakHoldDuration; + + QColor p_backgroundNominalColor; + QColor p_backgroundWarningColor; + QColor p_backgroundErrorColor; + QColor p_foregroundNominalColor; + QColor p_foregroundWarningColor; + QColor p_foregroundErrorColor; + + uint64_t lastRedrawTime = 0; + int channels = 0; + bool clipping = false; + bool vertical; + bool muted = false; + +public: + explicit VolumeMeter(QWidget *parent = nullptr, obs_volmeter_t *obs_volmeter = nullptr, bool vertical = false); + ~VolumeMeter(); + + void setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]); + QRect getBarRect() const; + bool needLayoutChange(); + + QColor getBackgroundNominalColor() const; + void setBackgroundNominalColor(QColor c); + QColor getBackgroundWarningColor() const; + void setBackgroundWarningColor(QColor c); + QColor getBackgroundErrorColor() const; + void setBackgroundErrorColor(QColor c); + QColor getForegroundNominalColor() const; + void setForegroundNominalColor(QColor c); + QColor getForegroundWarningColor() const; + void setForegroundWarningColor(QColor c); + QColor getForegroundErrorColor() const; + void setForegroundErrorColor(QColor c); + + QColor getBackgroundNominalColorDisabled() const; + void setBackgroundNominalColorDisabled(QColor c); + QColor getBackgroundWarningColorDisabled() const; + void setBackgroundWarningColorDisabled(QColor c); + QColor getBackgroundErrorColorDisabled() const; + void setBackgroundErrorColorDisabled(QColor c); + QColor getForegroundNominalColorDisabled() const; + void setForegroundNominalColorDisabled(QColor c); + QColor getForegroundWarningColorDisabled() const; + void setForegroundWarningColorDisabled(QColor c); + QColor getForegroundErrorColorDisabled() const; + void setForegroundErrorColorDisabled(QColor c); + + QColor getClipColor() const; + void setClipColor(QColor c); + QColor getMagnitudeColor() const; + void setMagnitudeColor(QColor c); + QColor getMajorTickColor() const; + void setMajorTickColor(QColor c); + QColor getMinorTickColor() const; + void setMinorTickColor(QColor c); + int getMeterThickness() const; + void setMeterThickness(int v); + qreal getMeterFontScaling() const; + void setMeterFontScaling(qreal v); + qreal getMinimumLevel() const; + void setMinimumLevel(qreal v); + qreal getWarningLevel() const; + void setWarningLevel(qreal v); + qreal getErrorLevel() const; + void setErrorLevel(qreal v); + qreal getClipLevel() const; + void setClipLevel(qreal v); + qreal getMinimumInputLevel() const; + void setMinimumInputLevel(qreal v); + qreal getPeakDecayRate() const; + void setPeakDecayRate(qreal v); + qreal getMagnitudeIntegrationTime() const; + void setMagnitudeIntegrationTime(qreal v); + qreal getPeakHoldDuration() const; + void setPeakHoldDuration(qreal v); + qreal getInputPeakHoldDuration() const; + void setInputPeakHoldDuration(qreal v); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void wheelEvent(QWheelEvent *event) override; + +protected: + void paintEvent(QPaintEvent *event) override; + void changeEvent(QEvent *e) override; +}; + +class VolumeMeterTimer : public QTimer { + Q_OBJECT + +public: + inline VolumeMeterTimer() : QTimer() {} + + void AddVolControl(VolumeMeter *meter); + void RemoveVolControl(VolumeMeter *meter); + +protected: + void timerEvent(QTimerEvent *event) override; + QList volumeMeters; +}; + +class QLabel; +class VolumeSlider; +class MuteCheckBox; +class OBSSourceLabel; + +class VolControl : public QFrame { + Q_OBJECT + +private: + OBSSource source; + std::vector sigs; + OBSSourceLabel *nameLabel; + QLabel *volLabel; + VolumeMeter *volMeter; + VolumeSlider *slider; + MuteCheckBox *mute; + QPushButton *config = nullptr; + float levelTotal; + float levelCount; + OBSFader obs_fader; + OBSVolMeter obs_volmeter; + bool vertical; + QMenu *contextMenu; + + static void OBSVolumeChanged(void *param, float db); + static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); + static void OBSVolumeMuted(void *data, calldata_t *calldata); + static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); + + void EmitConfigClicked(); + +private slots: + void VolumeChanged(); + void VolumeMuted(bool muted); + void MixersOrMonitoringChanged(); + + void SetMuted(bool checked); + void SliderChanged(int vol); + void updateText(); + +signals: + void ConfigClicked(); + +public: + explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); + ~VolControl(); + + inline obs_source_t *GetSource() const { return source; } + + void SetMeterDecayRate(qreal q); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + + void EnableSlider(bool enable); + inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } + + void refreshColors(); +}; + +class VolumeSlider : public AbsoluteSlider { + Q_OBJECT + +public: + obs_fader_t *fad; + + VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); + VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); + + bool getDisplayTicks() const; + void setDisplayTicks(bool display); + +private: + bool displayTicks = false; + QColor tickColor; + +protected: + virtual void paintEvent(QPaintEvent *event) override; +}; + +class VolumeAccessibleInterface : public QAccessibleWidget { + +public: + VolumeAccessibleInterface(QWidget *w); + + QVariant currentValue() const; + void setCurrentValue(const QVariant &value); + + QVariant maximumValue() const; + QVariant minimumValue() const; + + QVariant minimumStepSize() const; + +private: + VolumeSlider *slider() const; + +protected: + virtual QAccessible::Role role() const override; + virtual QString text(QAccessible::Text t) const override; +}; diff --git a/frontend/widgets/VolumeAccessibleInterface.cpp b/frontend/widgets/VolumeAccessibleInterface.cpp new file mode 100644 index 000000000..cd00c14d7 --- /dev/null +++ b/frontend/widgets/VolumeAccessibleInterface.cpp @@ -0,0 +1,1507 @@ +#include "window-basic-main.hpp" +#include "moc_volume-control.cpp" +#include "obs-app.hpp" +#include "mute-checkbox.hpp" +#include "absolute-slider.hpp" +#include "source-label.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +#define FADER_PRECISION 4096.0 + +// Size of the audio indicator in pixels +#define INDICATOR_THICKNESS 3 + +// Padding on top and bottom of vertical meters +#define METER_PADDING 1 + +std::weak_ptr VolumeMeter::updateTimer; + +static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) +{ + if (muted) + return Qt::Checked; + else if (unassigned) + return Qt::PartiallyChecked; + else + return Qt::Unchecked; +} + +static inline bool IsSourceUnassigned(obs_source_t *source) +{ + uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); + obs_monitoring_type mt = obs_source_get_monitoring_type(source); + + return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; +} + +static void ShowUnassignedWarning(const char *name) +{ + auto msgBox = [=]() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); + msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); +} + +void VolControl::OBSVolumeChanged(void *data, float db) +{ + Q_UNUSED(db); + VolControl *volControl = static_cast(data); + + QMetaObject::invokeMethod(volControl, "VolumeChanged"); +} + +void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + VolControl *volControl = static_cast(data); + + volControl->volMeter->setLevels(magnitude, peak, inputPeak); +} + +void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) +{ + VolControl *volControl = static_cast(data); + bool muted = calldata_bool(calldata, "muted"); + + QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); +} + +void VolControl::VolumeChanged() +{ + slider->blockSignals(true); + slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); + slider->blockSignals(false); + + updateText(); +} + +void VolControl::VolumeMuted(bool muted) +{ + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) +{ + + VolControl *volControl = static_cast(data); + QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); +} + +void VolControl::MixersOrMonitoringChanged() +{ + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::SetMuted(bool) +{ + bool checked = mute->checkState() == Qt::Checked; + bool prev = obs_source_muted(source); + obs_source_set_muted(source, checked); + bool unassigned = IsSourceUnassigned(source); + + if (!checked && unassigned) { + mute->setCheckState(Qt::PartiallyChecked); + /* Show notice about the source no being assigned to any tracks */ + bool has_shown_warning = + config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); + if (!has_shown_warning) + ShowUnassignedWarning(obs_source_get_name(source)); + } + + auto undo_redo = [](const std::string &uuid, bool val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_muted(source, val); + }; + + QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); + + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); +} + +void VolControl::SliderChanged(int vol) +{ + float prev = obs_source_get_volume(source); + + obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); + updateText(); + + auto undo_redo = [](const std::string &uuid, float val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_volume(source, val); + }; + + float val = obs_source_get_volume(source); + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), + std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); +} + +void VolControl::updateText() +{ + QString text; + float db = obs_fader_get_db(obs_fader); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + volLabel->setText(text); + + bool muted = obs_source_muted(source); + const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; + + QString sourceName = obs_source_get_name(source); + QString accText = QTStr(accTextLookup).arg(sourceName); + + slider->setAccessibleName(accText); +} + +void VolControl::EmitConfigClicked() +{ + emit ConfigClicked(); +} + +void VolControl::SetMeterDecayRate(qreal q) +{ + volMeter->setPeakDecayRate(q); +} + +void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + volMeter->setPeakMeterType(peakMeterType); +} + +VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) + : source(std::move(source_)), + levelTotal(0.0f), + levelCount(0.0f), + obs_fader(obs_fader_create(OBS_FADER_LOG)), + obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), + vertical(vertical), + contextMenu(nullptr) +{ + nameLabel = new OBSSourceLabel(source); + volLabel = new QLabel(); + mute = new MuteCheckBox(); + + volLabel->setObjectName("volLabel"); + volLabel->setAlignment(Qt::AlignCenter); + +#ifdef __APPLE__ + mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + QString sourceName = obs_source_get_name(source); + setObjectName(sourceName); + + if (showConfig) { + config = new QPushButton(this); + config->setProperty("class", "icon-dots-vert"); + config->setAutoDefault(false); + + config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); + + connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); + } + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + if (vertical) { + QHBoxLayout *nameLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QHBoxLayout *volLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QHBoxLayout *meterLayout = new QHBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, true); + slider = new VolumeSlider(obs_fader, Qt::Vertical); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + nameLayout->setAlignment(Qt::AlignCenter); + meterLayout->setAlignment(Qt::AlignCenter); + controlLayout->setAlignment(Qt::AlignCenter); + volLayout->setAlignment(Qt::AlignCenter); + + meterFrame->setObjectName("volMeterFrame"); + + nameLayout->setContentsMargins(0, 0, 0, 0); + nameLayout->setSpacing(0); + nameLayout->addWidget(nameLabel); + + controlLayout->setContentsMargins(0, 0, 0, 0); + controlLayout->setSpacing(0); + + // Add Headphone (audio monitoring) widget here + controlLayout->addWidget(mute); + + if (showConfig) { + controlLayout->addWidget(config); + } + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + meterLayout->addWidget(slider); + meterLayout->addWidget(volMeter); + + meterFrame->setLayout(meterLayout); + + volLayout->setContentsMargins(0, 0, 0, 0); + volLayout->setSpacing(0); + volLayout->addWidget(volLabel); + volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); + + mainLayout->addItem(nameLayout); + mainLayout->addItem(volLayout); + mainLayout->addWidget(meterFrame); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + + // Default size can cause clipping of long names in vertical layout. + QFont font = nameLabel->font(); + QFontInfo info(font); + nameLabel->setFont(font); + + setMaximumWidth(110); + } else { + QHBoxLayout *textLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QVBoxLayout *meterLayout = new QVBoxLayout; + QVBoxLayout *buttonLayout = new QVBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, false); + volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + + slider = new VolumeSlider(obs_fader, Qt::Horizontal); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + textLayout->setContentsMargins(0, 0, 0, 0); + textLayout->addWidget(nameLabel); + textLayout->addWidget(volLabel); + textLayout->setAlignment(nameLabel, Qt::AlignLeft); + textLayout->setAlignment(volLabel, Qt::AlignRight); + + meterFrame->setObjectName("volMeterFrame"); + meterFrame->setLayout(meterLayout); + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + + meterLayout->addWidget(volMeter); + meterLayout->addWidget(slider); + + buttonLayout->setContentsMargins(0, 0, 0, 0); + buttonLayout->setSpacing(0); + + if (showConfig) { + buttonLayout->addWidget(config); + } + buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); + buttonLayout->addWidget(mute); + + controlLayout->addItem(buttonLayout); + controlLayout->addWidget(meterFrame); + + mainLayout->addItem(textLayout); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + } + + setLayout(mainLayout); + + nameLabel->setText(sourceName); + + slider->setMinimum(0); + slider->setMaximum(int(FADER_PRECISION)); + + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + mute->setCheckState(GetCheckState(muted, unassigned)); + volMeter->muted = muted || unassigned; + mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); + obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, + this); + + QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); + QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); + + obs_fader_attach_source(obs_fader, source); + obs_volmeter_attach_source(obs_volmeter, source); + + /* Call volume changed once to init the slider position and label */ + VolumeChanged(); +} + +void VolControl::EnableSlider(bool enable) +{ + slider->setEnabled(enable); +} + +VolControl::~VolControl() +{ + obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.clear(); + + if (contextMenu) + contextMenu->close(); +} + +static inline QColor color_from_int(long long val) +{ + QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); + color.setAlpha(255); + + return color; +} + +QColor VolumeMeter::getBackgroundNominalColor() const +{ + return p_backgroundNominalColor; +} + +QColor VolumeMeter::getBackgroundNominalColorDisabled() const +{ + return backgroundNominalColorDisabled; +} + +void VolumeMeter::setBackgroundNominalColor(QColor c) +{ + p_backgroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreen")); + } else { + backgroundNominalColor = p_backgroundNominalColor; + } +} + +void VolumeMeter::setBackgroundNominalColorDisabled(QColor c) +{ + backgroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundWarningColor() const +{ + return p_backgroundWarningColor; +} + +QColor VolumeMeter::getBackgroundWarningColorDisabled() const +{ + return backgroundWarningColorDisabled; +} + +void VolumeMeter::setBackgroundWarningColor(QColor c) +{ + p_backgroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellow")); + } else { + backgroundWarningColor = p_backgroundWarningColor; + } +} + +void VolumeMeter::setBackgroundWarningColorDisabled(QColor c) +{ + backgroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundErrorColor() const +{ + return p_backgroundErrorColor; +} + +QColor VolumeMeter::getBackgroundErrorColorDisabled() const +{ + return backgroundErrorColorDisabled; +} + +void VolumeMeter::setBackgroundErrorColor(QColor c) +{ + p_backgroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRed")); + } else { + backgroundErrorColor = p_backgroundErrorColor; + } +} + +void VolumeMeter::setBackgroundErrorColorDisabled(QColor c) +{ + backgroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundNominalColor() const +{ + return p_foregroundNominalColor; +} + +QColor VolumeMeter::getForegroundNominalColorDisabled() const +{ + return foregroundNominalColorDisabled; +} + +void VolumeMeter::setForegroundNominalColor(QColor c) +{ + p_foregroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreenActive")); + } else { + foregroundNominalColor = p_foregroundNominalColor; + } +} + +void VolumeMeter::setForegroundNominalColorDisabled(QColor c) +{ + foregroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundWarningColor() const +{ + return p_foregroundWarningColor; +} + +QColor VolumeMeter::getForegroundWarningColorDisabled() const +{ + return foregroundWarningColorDisabled; +} + +void VolumeMeter::setForegroundWarningColor(QColor c) +{ + p_foregroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellowActive")); + } else { + foregroundWarningColor = p_foregroundWarningColor; + } +} + +void VolumeMeter::setForegroundWarningColorDisabled(QColor c) +{ + foregroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundErrorColor() const +{ + return p_foregroundErrorColor; +} + +QColor VolumeMeter::getForegroundErrorColorDisabled() const +{ + return foregroundErrorColorDisabled; +} + +void VolumeMeter::setForegroundErrorColor(QColor c) +{ + p_foregroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRedActive")); + } else { + foregroundErrorColor = p_foregroundErrorColor; + } +} + +void VolumeMeter::setForegroundErrorColorDisabled(QColor c) +{ + foregroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getClipColor() const +{ + return clipColor; +} + +void VolumeMeter::setClipColor(QColor c) +{ + clipColor = std::move(c); +} + +QColor VolumeMeter::getMagnitudeColor() const +{ + return magnitudeColor; +} + +void VolumeMeter::setMagnitudeColor(QColor c) +{ + magnitudeColor = std::move(c); +} + +QColor VolumeMeter::getMajorTickColor() const +{ + return majorTickColor; +} + +void VolumeMeter::setMajorTickColor(QColor c) +{ + majorTickColor = std::move(c); +} + +QColor VolumeMeter::getMinorTickColor() const +{ + return minorTickColor; +} + +void VolumeMeter::setMinorTickColor(QColor c) +{ + minorTickColor = std::move(c); +} + +int VolumeMeter::getMeterThickness() const +{ + return meterThickness; +} + +void VolumeMeter::setMeterThickness(int v) +{ + meterThickness = v; + recalculateLayout = true; +} + +qreal VolumeMeter::getMeterFontScaling() const +{ + return meterFontScaling; +} + +void VolumeMeter::setMeterFontScaling(qreal v) +{ + meterFontScaling = v; + recalculateLayout = true; +} + +void VolControl::refreshColors() +{ + volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); + volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); + volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); + volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); + volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); + volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); +} + +qreal VolumeMeter::getMinimumLevel() const +{ + return minimumLevel; +} + +void VolumeMeter::setMinimumLevel(qreal v) +{ + minimumLevel = v; +} + +qreal VolumeMeter::getWarningLevel() const +{ + return warningLevel; +} + +void VolumeMeter::setWarningLevel(qreal v) +{ + warningLevel = v; +} + +qreal VolumeMeter::getErrorLevel() const +{ + return errorLevel; +} + +void VolumeMeter::setErrorLevel(qreal v) +{ + errorLevel = v; +} + +qreal VolumeMeter::getClipLevel() const +{ + return clipLevel; +} + +void VolumeMeter::setClipLevel(qreal v) +{ + clipLevel = v; +} + +qreal VolumeMeter::getMinimumInputLevel() const +{ + return minimumInputLevel; +} + +void VolumeMeter::setMinimumInputLevel(qreal v) +{ + minimumInputLevel = v; +} + +qreal VolumeMeter::getPeakDecayRate() const +{ + return peakDecayRate; +} + +void VolumeMeter::setPeakDecayRate(qreal v) +{ + peakDecayRate = v; +} + +qreal VolumeMeter::getMagnitudeIntegrationTime() const +{ + return magnitudeIntegrationTime; +} + +void VolumeMeter::setMagnitudeIntegrationTime(qreal v) +{ + magnitudeIntegrationTime = v; +} + +qreal VolumeMeter::getPeakHoldDuration() const +{ + return peakHoldDuration; +} + +void VolumeMeter::setPeakHoldDuration(qreal v) +{ + peakHoldDuration = v; +} + +qreal VolumeMeter::getInputPeakHoldDuration() const +{ + return inputPeakHoldDuration; +} + +void VolumeMeter::setInputPeakHoldDuration(qreal v) +{ + inputPeakHoldDuration = v; +} + +void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); + switch (peakMeterType) { + case TRUE_PEAK_METER: + // For true-peak meters EBU has defined the Permitted Maximum, + // taking into account the accuracy of the meter and further + // processing required by lossy audio compression. + // + // The alignment level was not specified, but I've adjusted + // it compared to a sample-peak meter. Incidentally Youtube + // uses this new Alignment Level as the maximum integrated + // loudness of a video. + // + // * Permitted Maximum Level (PML) = -2.0 dBTP + // * Alignment Level (AL) = -13 dBTP + setErrorLevel(-2.0); + setWarningLevel(-13.0); + break; + + case SAMPLE_PEAK_METER: + default: + // For a sample Peak Meter EBU has the following level + // definitions, taking into account inaccuracies of this meter: + // + // * Permitted Maximum Level (PML) = -9.0 dBFS + // * Alignment Level (AL) = -20.0 dBFS + setErrorLevel(-9.0); + setWarningLevel(-20.0); + break; + } +} + +void VolumeMeter::mousePressEvent(QMouseEvent *event) +{ + setFocus(Qt::MouseFocusReason); + event->accept(); +} + +void VolumeMeter::wheelEvent(QWheelEvent *event) +{ + QApplication::sendEvent(focusProxy(), event); +} + +VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, bool vertical) + : QWidget(parent), + obs_volmeter(obs_volmeter), + vertical(vertical) +{ + setAttribute(Qt::WA_OpaquePaintEvent, true); + + // Default meter settings, they only show if + // there is no stylesheet, do not remove. + backgroundNominalColor.setRgb(0x26, 0x7f, 0x26); // Dark green + backgroundWarningColor.setRgb(0x7f, 0x7f, 0x26); // Dark yellow + backgroundErrorColor.setRgb(0x7f, 0x26, 0x26); // Dark red + foregroundNominalColor.setRgb(0x4c, 0xff, 0x4c); // Bright green + foregroundWarningColor.setRgb(0xff, 0xff, 0x4c); // Bright yellow + foregroundErrorColor.setRgb(0xff, 0x4c, 0x4c); // Bright red + + backgroundNominalColorDisabled.setRgb(90, 90, 90); + backgroundWarningColorDisabled.setRgb(117, 117, 117); + backgroundErrorColorDisabled.setRgb(65, 65, 65); + foregroundNominalColorDisabled.setRgb(163, 163, 163); + foregroundWarningColorDisabled.setRgb(217, 217, 217); + foregroundErrorColorDisabled.setRgb(113, 113, 113); + + clipColor.setRgb(0xff, 0xff, 0xff); // Bright white + magnitudeColor.setRgb(0x00, 0x00, 0x00); // Black + majorTickColor.setRgb(0x00, 0x00, 0x00); // Black + minorTickColor.setRgb(0x32, 0x32, 0x32); // Dark gray + minimumLevel = -60.0; // -60 dB + warningLevel = -20.0; // -20 dB + errorLevel = -9.0; // -9 dB + clipLevel = -0.5; // -0.5 dB + minimumInputLevel = -50.0; // -50 dB + peakDecayRate = 11.76; // 20 dB / 1.7 sec + magnitudeIntegrationTime = 0.3; // 99% in 300 ms + peakHoldDuration = 20.0; // 20 seconds + inputPeakHoldDuration = 1.0; // 1 second + meterThickness = 3; // Bar thickness in pixels + meterFontScaling = 0.7; // Font size for numbers is 70% of Widget's font size + channels = (int)audio_output_get_channels(obs_get_audio()); + + doLayout(); + updateTimerRef = updateTimer.lock(); + if (!updateTimerRef) { + updateTimerRef = std::make_shared(); + updateTimerRef->setTimerType(Qt::PreciseTimer); + updateTimerRef->start(16); + updateTimer = updateTimerRef; + } + + updateTimerRef->AddVolControl(this); +} + +VolumeMeter::~VolumeMeter() +{ + updateTimerRef->RemoveVolControl(this); +} + +void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + uint64_t ts = os_gettime_ns(); + QMutexLocker locker(&dataMutex); + + currentLastUpdateTime = ts; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = magnitude[channelNr]; + currentPeak[channelNr] = peak[channelNr]; + currentInputPeak[channelNr] = inputPeak[channelNr]; + } + + // In case there are more updates then redraws we must make sure + // that the ballistics of peak and hold are recalculated. + locker.unlock(); + calculateBallistics(ts); +} + +inline void VolumeMeter::resetLevels() +{ + currentLastUpdateTime = 0; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = -M_INFINITE; + currentPeak[channelNr] = -M_INFINITE; + currentInputPeak[channelNr] = -M_INFINITE; + + displayMagnitude[channelNr] = -M_INFINITE; + displayPeak[channelNr] = -M_INFINITE; + displayPeakHold[channelNr] = -M_INFINITE; + displayPeakHoldLastUpdateTime[channelNr] = 0; + displayInputPeakHold[channelNr] = -M_INFINITE; + displayInputPeakHoldLastUpdateTime[channelNr] = 0; + } +} + +bool VolumeMeter::needLayoutChange() +{ + int currentNrAudioChannels = obs_volmeter_get_nr_channels(obs_volmeter); + + if (!currentNrAudioChannels) { + struct obs_audio_info oai; + obs_get_audio_info(&oai); + currentNrAudioChannels = (oai.speakers == SPEAKERS_MONO) ? 1 : 2; + } + + if (displayNrAudioChannels != currentNrAudioChannels) { + displayNrAudioChannels = currentNrAudioChannels; + recalculateLayout = true; + } + + return recalculateLayout; +} + +// When this is called from the constructor, obs_volmeter_get_nr_channels has not +// yet been called and Q_PROPERTY settings have not yet been read from the +// stylesheet. +inline void VolumeMeter::doLayout() +{ + QMutexLocker locker(&dataMutex); + + if (displayNrAudioChannels) { + int meterSize = std::floor(22 / displayNrAudioChannels); + setMeterThickness(std::clamp(meterSize, 3, 7)); + } + recalculateLayout = false; + + tickFont = font(); + QFontInfo info(tickFont); + tickFont.setPointSizeF(info.pointSizeF() * meterFontScaling); + QFontMetrics metrics(tickFont); + if (vertical) { + // Each meter channel is meterThickness pixels wide, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, space to hold our longest label in this font, + // and a few pixels before the fader. + QRect scaleBounds = metrics.boundingRect("-88"); + setMinimumSize(displayNrAudioChannels * (meterThickness + 1) - 1 + 10 + scaleBounds.width() + 2, 100); + } else { + // Each meter channel is meterThickness pixels high, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, and space high enough to hold our label in + // this font, presuming that digits don't have descenders. + setMinimumSize(100, displayNrAudioChannels * (meterThickness + 1) - 1 + 4 + metrics.capHeight()); + } + + resetLevels(); +} + +inline bool VolumeMeter::detectIdle(uint64_t ts) +{ + double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; + if (timeSinceLastUpdate > 0.5) { + resetLevels(); + return true; + } else { + return false; + } +} + +inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw) +{ + if (currentPeak[channelNr] >= displayPeak[channelNr] || isnan(displayPeak[channelNr])) { + // Attack of peak is immediate. + displayPeak[channelNr] = currentPeak[channelNr]; + } else { + // Decay of peak is 40 dB / 1.7 seconds for Fast Profile + // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) + // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) + float decay = float(peakDecayRate * timeSinceLastRedraw); + displayPeak[channelNr] = + std::clamp(displayPeak[channelNr] - decay, std::min(currentPeak[channelNr], 0.f), 0.f); + } + + if (currentPeak[channelNr] >= displayPeakHold[channelNr] || !isfinite(displayPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak + // after 20 seconds. + qreal timeSinceLastPeak = (uint64_t)(ts - displayPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > peakHoldDuration) { + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (currentInputPeak[channelNr] >= displayInputPeakHold[channelNr] || + !isfinite(displayInputPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak after 1 second. + qreal timeSinceLastPeak = (uint64_t)(ts - displayInputPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > inputPeakHoldDuration) { + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (!isfinite(displayMagnitude[channelNr])) { + // The statements in the else-leg do not work with + // NaN and infinite displayMagnitude. + displayMagnitude[channelNr] = currentMagnitude[channelNr]; + } else { + // A VU meter will integrate to the new value to 99% in 300 ms. + // The calculation here is very simplified and is more accurate + // with higher frame-rate. + float attack = float((currentMagnitude[channelNr] - displayMagnitude[channelNr]) * + (timeSinceLastRedraw / magnitudeIntegrationTime) * 0.99); + displayMagnitude[channelNr] = + std::clamp(displayMagnitude[channelNr] + attack, (float)minimumLevel, 0.f); + } +} + +inline void VolumeMeter::calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw) +{ + QMutexLocker locker(&dataMutex); + + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) + calculateBallisticsForChannel(channelNr, ts, timeSinceLastRedraw); +} + +void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold) +{ + QMutexLocker locker(&dataMutex); + QColor color; + + if (peakHold < minimumInputLevel) + color = backgroundNominalColor; + else if (peakHold < warningLevel) + color = foregroundNominalColor; + else if (peakHold < errorLevel) + color = foregroundWarningColor; + else if (peakHold <= clipLevel) + color = foregroundErrorColor; + else + color = clipColor; + + painter.fillRect(x, y, width, height, color); +} + +void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width) +{ + qreal scale = width / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = int(x + width - (i * scale) - 1); + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + QRect textBounds = metrics.boundingRect(str); + int pos; + if (i == 0) { + pos = position - textBounds.width(); + } else { + pos = position - (textBounds.width() / 2); + if (pos < 0) + pos = 0; + } + painter.drawText(pos, y + 4 + metrics.capHeight(), str); + + painter.drawLine(position, y, position, y + 2); + } +} + +void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) +{ + qreal scale = height / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = y + int(i * scale) + METER_PADDING; + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + if (i == 0) { + painter.drawText(x + 10, position + metrics.capHeight(), str); + } else { + painter.drawText(x + 8, position + (metrics.capHeight() / 2), str); + } + + painter.drawLine(x, position, x + 2, position); + } +} + +#define CLIP_FLASH_DURATION_MS 1000 + +inline int VolumeMeter::convertToInt(float number) +{ + constexpr int min = std::numeric_limits::min(); + constexpr int max = std::numeric_limits::max(); + + // NOTE: Conversion from 'const int' to 'float' changes max value from 2147483647 to 2147483648 + if (number >= (float)max) + return max; + else if (number < min) + return min; + else + return int(number); +} + +void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = width / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = x + 0; + int maximumPosition = x + width; + int magnitudePosition = x + width - convertToInt(magnitude * scale); + int peakPosition = x + width - convertToInt(peak * scale); + int peakHoldPosition = x + width - convertToInt(peakHold * scale); + int warningPosition = x + width - convertToInt(warningLevel * scale); + int errorPosition = x + width - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(minimumPosition, y, peakPosition - minimumPosition, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(peakPosition, y, warningPosition - peakPosition, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, peakPosition - warningPosition, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(peakPosition, y, errorPosition - peakPosition, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(errorPosition, y, peakPosition - errorPosition, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(peakPosition, y, maximumPosition - peakPosition, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(minimumPosition, y, end, height, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(magnitudePosition - 3, y, 3, height, magnitudeColor); +} + +void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = height / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = y + 0; + int maximumPosition = y + height; + int magnitudePosition = y + height - convertToInt(magnitude * scale); + int peakPosition = y + height - convertToInt(peak * scale); + int peakHoldPosition = y + height - convertToInt(peakHold * scale); + int warningPosition = y + height - convertToInt(warningLevel * scale); + int errorPosition = y + height - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(x, minimumPosition, width, peakPosition - minimumPosition, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, peakPosition, width, warningPosition - peakPosition, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, peakPosition - warningPosition, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, peakPosition, width, errorPosition - peakPosition, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, errorPosition, width, peakPosition - errorPosition, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(x, peakPosition, width, maximumPosition - peakPosition, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(x, minimumPosition, width, end, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(x, magnitudePosition - 3, width, 3, magnitudeColor); +} + +void VolumeMeter::paintEvent(QPaintEvent *event) +{ + uint64_t ts = os_gettime_ns(); + qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; + calculateBallistics(ts, timeSinceLastRedraw); + bool idle = detectIdle(ts); + + QRect widgetRect = rect(); + int width = widgetRect.width(); + int height = widgetRect.height(); + + QPainter painter(this); + + // Paint window background color (as widget is opaque) + QColor background = palette().color(QPalette::ColorRole::Window); + painter.fillRect(event->region().boundingRect(), background); + + if (vertical) + height -= METER_PADDING * 2; + + // timerEvent requests update of the bar(s) only, so we can avoid the + // overhead of repainting the scale and labels. + if (event->region().boundingRect() != getBarRect()) { + if (needLayoutChange()) + doLayout(); + + if (vertical) { + paintVTicks(painter, displayNrAudioChannels * (meterThickness + 1) - 1, 0, + height - (INDICATOR_THICKNESS + 3)); + } else { + paintHTicks(painter, INDICATOR_THICKNESS + 3, displayNrAudioChannels * (meterThickness + 1) - 1, + width - (INDICATOR_THICKNESS + 3)); + } + } + + if (vertical) { + // Invert the Y axis to ease the math + painter.translate(0, height + METER_PADDING); + painter.scale(1, -1); + } + + for (int channelNr = 0; channelNr < displayNrAudioChannels; channelNr++) { + + int channelNrFixed = (displayNrAudioChannels == 1 && channels > 2) ? 2 : channelNr; + + if (vertical) + paintVMeter(painter, channelNr * (meterThickness + 1), INDICATOR_THICKNESS + 2, meterThickness, + height - (INDICATOR_THICKNESS + 2), displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + else + paintHMeter(painter, INDICATOR_THICKNESS + 2, channelNr * (meterThickness + 1), + width - (INDICATOR_THICKNESS + 2), meterThickness, displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + + if (idle) + continue; + + // By not drawing the input meter boxes the user can + // see that the audio stream has been stopped, without + // having too much visual impact. + if (vertical) + paintInputMeter(painter, channelNr * (meterThickness + 1), 0, meterThickness, + INDICATOR_THICKNESS, displayInputPeakHold[channelNrFixed]); + else + paintInputMeter(painter, 0, channelNr * (meterThickness + 1), INDICATOR_THICKNESS, + meterThickness, displayInputPeakHold[channelNrFixed]); + } + + lastRedrawTime = ts; +} + +QRect VolumeMeter::getBarRect() const +{ + QRect rec = rect(); + if (vertical) + rec.setWidth(displayNrAudioChannels * (meterThickness + 1) - 1); + else + rec.setHeight(displayNrAudioChannels * (meterThickness + 1) - 1); + + return rec; +} + +void VolumeMeter::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::StyleChange) + recalculateLayout = true; + + QWidget::changeEvent(e); +} + +void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) +{ + volumeMeters.push_back(meter); +} + +void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) +{ + volumeMeters.removeOne(meter); +} + +void VolumeMeterTimer::timerEvent(QTimerEvent *) +{ + for (VolumeMeter *meter : volumeMeters) { + if (meter->needLayoutChange()) { + // Tell paintEvent to update layout and paint everything + meter->update(); + } else { + // Tell paintEvent to paint only the bars + meter->update(meter->getBarRect()); + } + } +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) +{ + fad = fader; +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) + : AbsoluteSlider(orientation, parent) +{ + fad = fader; +} + +bool VolumeSlider::getDisplayTicks() const +{ + return displayTicks; +} + +void VolumeSlider::setDisplayTicks(bool display) +{ + displayTicks = display; +} + +void VolumeSlider::paintEvent(QPaintEvent *event) +{ + if (!getDisplayTicks()) { + QSlider::paintEvent(event); + return; + } + + QPainter painter(this); + QColor tickColor(91, 98, 115, 255); + + obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); + + QStyleOptionSlider opt; + initStyleOption(&opt); + + QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); + QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); + + if (orientation() == Qt::Horizontal) { + const int sliderWidth = groove.width() - handle.width(); + + float tickLength = groove.height() * 1.5; + tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); + + float yPos = groove.center().y() - (tickLength / 2) + 1; + + for (int db = -10; db >= -90; db -= 10) { + float tickValue = fader_db_to_def(db); + + float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); + painter.fillRect(xPos, yPos, 1, tickLength, tickColor); + } + } + + if (orientation() == Qt::Vertical) { + const int sliderHeight = groove.height() - handle.height(); + + float tickLength = groove.width() * 1.5; + tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); + + float xPos = groove.center().x() - (tickLength / 2) + 1; + + for (int db = -10; db >= -96; db -= 10) { + float tickValue = fader_db_to_def(db); + + float yPos = + groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); + painter.fillRect(xPos, yPos, tickLength, 1, tickColor); + } + } + + QSlider::paintEvent(event); +} + +VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} + +VolumeSlider *VolumeAccessibleInterface::slider() const +{ + return qobject_cast(object()); +} + +QString VolumeAccessibleInterface::text(QAccessible::Text t) const +{ + if (slider()->isVisible()) { + switch (t) { + case QAccessible::Text::Value: + return currentValue().toString(); + default: + break; + } + } + return QAccessibleWidget::text(t); +} + +QVariant VolumeAccessibleInterface::currentValue() const +{ + QString text; + float db = obs_fader_get_db(slider()->fad); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + return text; +} + +void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) +{ + slider()->setValue(value.toInt()); +} + +QVariant VolumeAccessibleInterface::maximumValue() const +{ + return slider()->maximum(); +} + +QVariant VolumeAccessibleInterface::minimumValue() const +{ + return slider()->minimum(); +} + +QVariant VolumeAccessibleInterface::minimumStepSize() const +{ + return slider()->singleStep(); +} + +QAccessible::Role VolumeAccessibleInterface::role() const +{ + return QAccessible::Role::Slider; +} diff --git a/frontend/widgets/VolumeAccessibleInterface.hpp b/frontend/widgets/VolumeAccessibleInterface.hpp new file mode 100644 index 000000000..51c77f247 --- /dev/null +++ b/frontend/widgets/VolumeAccessibleInterface.hpp @@ -0,0 +1,341 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "absolute-slider.hpp" + +class QPushButton; +class VolumeMeterTimer; +class VolumeSlider; + +class VolumeMeter : public QWidget { + Q_OBJECT + Q_PROPERTY(QColor backgroundNominalColor READ getBackgroundNominalColor WRITE setBackgroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColor READ getBackgroundWarningColor WRITE setBackgroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor backgroundErrorColor READ getBackgroundErrorColor WRITE setBackgroundErrorColor DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColor READ getForegroundNominalColor WRITE setForegroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColor READ getForegroundWarningColor WRITE setForegroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor foregroundErrorColor READ getForegroundErrorColor WRITE setForegroundErrorColor DESIGNABLE true) + + Q_PROPERTY(QColor backgroundNominalColorDisabled READ getBackgroundNominalColorDisabled WRITE + setBackgroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColorDisabled READ getBackgroundWarningColorDisabled WRITE + setBackgroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundErrorColorDisabled READ getBackgroundErrorColorDisabled WRITE + setBackgroundErrorColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColorDisabled READ getForegroundNominalColorDisabled WRITE + setForegroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColorDisabled READ getForegroundWarningColorDisabled WRITE + setForegroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundErrorColorDisabled READ getForegroundErrorColorDisabled WRITE + setForegroundErrorColorDisabled DESIGNABLE true) + + Q_PROPERTY(QColor clipColor READ getClipColor WRITE setClipColor DESIGNABLE true) + Q_PROPERTY(QColor magnitudeColor READ getMagnitudeColor WRITE setMagnitudeColor DESIGNABLE true) + Q_PROPERTY(QColor majorTickColor READ getMajorTickColor WRITE setMajorTickColor DESIGNABLE true) + Q_PROPERTY(QColor minorTickColor READ getMinorTickColor WRITE setMinorTickColor DESIGNABLE true) + Q_PROPERTY(int meterThickness READ getMeterThickness WRITE setMeterThickness DESIGNABLE true) + Q_PROPERTY(qreal meterFontScaling READ getMeterFontScaling WRITE setMeterFontScaling DESIGNABLE true) + + // Levels are denoted in dBFS. + Q_PROPERTY(qreal minimumLevel READ getMinimumLevel WRITE setMinimumLevel DESIGNABLE true) + Q_PROPERTY(qreal warningLevel READ getWarningLevel WRITE setWarningLevel DESIGNABLE true) + Q_PROPERTY(qreal errorLevel READ getErrorLevel WRITE setErrorLevel DESIGNABLE true) + Q_PROPERTY(qreal clipLevel READ getClipLevel WRITE setClipLevel DESIGNABLE true) + Q_PROPERTY(qreal minimumInputLevel READ getMinimumInputLevel WRITE setMinimumInputLevel DESIGNABLE true) + + // Rates are denoted in dB/second. + Q_PROPERTY(qreal peakDecayRate READ getPeakDecayRate WRITE setPeakDecayRate DESIGNABLE true) + + // Time in seconds for the VU meter to integrate over. + Q_PROPERTY(qreal magnitudeIntegrationTime READ getMagnitudeIntegrationTime WRITE setMagnitudeIntegrationTime + DESIGNABLE true) + + // Duration is denoted in seconds. + Q_PROPERTY(qreal peakHoldDuration READ getPeakHoldDuration WRITE setPeakHoldDuration DESIGNABLE true) + Q_PROPERTY(qreal inputPeakHoldDuration READ getInputPeakHoldDuration WRITE setInputPeakHoldDuration + DESIGNABLE true) + + friend class VolControl; + +private: + obs_volmeter_t *obs_volmeter; + static std::weak_ptr updateTimer; + std::shared_ptr updateTimerRef; + + inline void resetLevels(); + inline void doLayout(); + inline bool detectIdle(uint64_t ts); + inline void calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw = 0.0); + inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw); + + inline int convertToInt(float number); + void paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold); + void paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintHTicks(QPainter &painter, int x, int y, int width); + void paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintVTicks(QPainter &painter, int x, int y, int height); + + QMutex dataMutex; + + bool recalculateLayout = true; + uint64_t currentLastUpdateTime = 0; + float currentMagnitude[MAX_AUDIO_CHANNELS]; + float currentPeak[MAX_AUDIO_CHANNELS]; + float currentInputPeak[MAX_AUDIO_CHANNELS]; + + int displayNrAudioChannels = 0; + float displayMagnitude[MAX_AUDIO_CHANNELS]; + float displayPeak[MAX_AUDIO_CHANNELS]; + float displayPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + float displayInputPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayInputPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + + QFont tickFont; + QColor backgroundNominalColor; + QColor backgroundWarningColor; + QColor backgroundErrorColor; + QColor foregroundNominalColor; + QColor foregroundWarningColor; + QColor foregroundErrorColor; + + QColor backgroundNominalColorDisabled; + QColor backgroundWarningColorDisabled; + QColor backgroundErrorColorDisabled; + QColor foregroundNominalColorDisabled; + QColor foregroundWarningColorDisabled; + QColor foregroundErrorColorDisabled; + + QColor clipColor; + QColor magnitudeColor; + QColor majorTickColor; + QColor minorTickColor; + + int meterThickness; + qreal meterFontScaling; + + qreal minimumLevel; + qreal warningLevel; + qreal errorLevel; + qreal clipLevel; + qreal minimumInputLevel; + qreal peakDecayRate; + qreal magnitudeIntegrationTime; + qreal peakHoldDuration; + qreal inputPeakHoldDuration; + + QColor p_backgroundNominalColor; + QColor p_backgroundWarningColor; + QColor p_backgroundErrorColor; + QColor p_foregroundNominalColor; + QColor p_foregroundWarningColor; + QColor p_foregroundErrorColor; + + uint64_t lastRedrawTime = 0; + int channels = 0; + bool clipping = false; + bool vertical; + bool muted = false; + +public: + explicit VolumeMeter(QWidget *parent = nullptr, obs_volmeter_t *obs_volmeter = nullptr, bool vertical = false); + ~VolumeMeter(); + + void setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]); + QRect getBarRect() const; + bool needLayoutChange(); + + QColor getBackgroundNominalColor() const; + void setBackgroundNominalColor(QColor c); + QColor getBackgroundWarningColor() const; + void setBackgroundWarningColor(QColor c); + QColor getBackgroundErrorColor() const; + void setBackgroundErrorColor(QColor c); + QColor getForegroundNominalColor() const; + void setForegroundNominalColor(QColor c); + QColor getForegroundWarningColor() const; + void setForegroundWarningColor(QColor c); + QColor getForegroundErrorColor() const; + void setForegroundErrorColor(QColor c); + + QColor getBackgroundNominalColorDisabled() const; + void setBackgroundNominalColorDisabled(QColor c); + QColor getBackgroundWarningColorDisabled() const; + void setBackgroundWarningColorDisabled(QColor c); + QColor getBackgroundErrorColorDisabled() const; + void setBackgroundErrorColorDisabled(QColor c); + QColor getForegroundNominalColorDisabled() const; + void setForegroundNominalColorDisabled(QColor c); + QColor getForegroundWarningColorDisabled() const; + void setForegroundWarningColorDisabled(QColor c); + QColor getForegroundErrorColorDisabled() const; + void setForegroundErrorColorDisabled(QColor c); + + QColor getClipColor() const; + void setClipColor(QColor c); + QColor getMagnitudeColor() const; + void setMagnitudeColor(QColor c); + QColor getMajorTickColor() const; + void setMajorTickColor(QColor c); + QColor getMinorTickColor() const; + void setMinorTickColor(QColor c); + int getMeterThickness() const; + void setMeterThickness(int v); + qreal getMeterFontScaling() const; + void setMeterFontScaling(qreal v); + qreal getMinimumLevel() const; + void setMinimumLevel(qreal v); + qreal getWarningLevel() const; + void setWarningLevel(qreal v); + qreal getErrorLevel() const; + void setErrorLevel(qreal v); + qreal getClipLevel() const; + void setClipLevel(qreal v); + qreal getMinimumInputLevel() const; + void setMinimumInputLevel(qreal v); + qreal getPeakDecayRate() const; + void setPeakDecayRate(qreal v); + qreal getMagnitudeIntegrationTime() const; + void setMagnitudeIntegrationTime(qreal v); + qreal getPeakHoldDuration() const; + void setPeakHoldDuration(qreal v); + qreal getInputPeakHoldDuration() const; + void setInputPeakHoldDuration(qreal v); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void wheelEvent(QWheelEvent *event) override; + +protected: + void paintEvent(QPaintEvent *event) override; + void changeEvent(QEvent *e) override; +}; + +class VolumeMeterTimer : public QTimer { + Q_OBJECT + +public: + inline VolumeMeterTimer() : QTimer() {} + + void AddVolControl(VolumeMeter *meter); + void RemoveVolControl(VolumeMeter *meter); + +protected: + void timerEvent(QTimerEvent *event) override; + QList volumeMeters; +}; + +class QLabel; +class VolumeSlider; +class MuteCheckBox; +class OBSSourceLabel; + +class VolControl : public QFrame { + Q_OBJECT + +private: + OBSSource source; + std::vector sigs; + OBSSourceLabel *nameLabel; + QLabel *volLabel; + VolumeMeter *volMeter; + VolumeSlider *slider; + MuteCheckBox *mute; + QPushButton *config = nullptr; + float levelTotal; + float levelCount; + OBSFader obs_fader; + OBSVolMeter obs_volmeter; + bool vertical; + QMenu *contextMenu; + + static void OBSVolumeChanged(void *param, float db); + static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); + static void OBSVolumeMuted(void *data, calldata_t *calldata); + static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); + + void EmitConfigClicked(); + +private slots: + void VolumeChanged(); + void VolumeMuted(bool muted); + void MixersOrMonitoringChanged(); + + void SetMuted(bool checked); + void SliderChanged(int vol); + void updateText(); + +signals: + void ConfigClicked(); + +public: + explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); + ~VolControl(); + + inline obs_source_t *GetSource() const { return source; } + + void SetMeterDecayRate(qreal q); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + + void EnableSlider(bool enable); + inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } + + void refreshColors(); +}; + +class VolumeSlider : public AbsoluteSlider { + Q_OBJECT + +public: + obs_fader_t *fad; + + VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); + VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); + + bool getDisplayTicks() const; + void setDisplayTicks(bool display); + +private: + bool displayTicks = false; + QColor tickColor; + +protected: + virtual void paintEvent(QPaintEvent *event) override; +}; + +class VolumeAccessibleInterface : public QAccessibleWidget { + +public: + VolumeAccessibleInterface(QWidget *w); + + QVariant currentValue() const; + void setCurrentValue(const QVariant &value); + + QVariant maximumValue() const; + QVariant minimumValue() const; + + QVariant minimumStepSize() const; + +private: + VolumeSlider *slider() const; + +protected: + virtual QAccessible::Role role() const override; + virtual QString text(QAccessible::Text t) const override; +}; diff --git a/frontend/widgets/VolumeMeter.cpp b/frontend/widgets/VolumeMeter.cpp new file mode 100644 index 000000000..cd00c14d7 --- /dev/null +++ b/frontend/widgets/VolumeMeter.cpp @@ -0,0 +1,1507 @@ +#include "window-basic-main.hpp" +#include "moc_volume-control.cpp" +#include "obs-app.hpp" +#include "mute-checkbox.hpp" +#include "absolute-slider.hpp" +#include "source-label.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +#define FADER_PRECISION 4096.0 + +// Size of the audio indicator in pixels +#define INDICATOR_THICKNESS 3 + +// Padding on top and bottom of vertical meters +#define METER_PADDING 1 + +std::weak_ptr VolumeMeter::updateTimer; + +static inline Qt::CheckState GetCheckState(bool muted, bool unassigned) +{ + if (muted) + return Qt::Checked; + else if (unassigned) + return Qt::PartiallyChecked; + else + return Qt::Unchecked; +} + +static inline bool IsSourceUnassigned(obs_source_t *source) +{ + uint32_t mixes = (obs_source_get_audio_mixers(source) & ((1 << MAX_AUDIO_MIXES) - 1)); + obs_monitoring_type mt = obs_source_get_monitoring_type(source); + + return mixes == 0 && mt != OBS_MONITORING_TYPE_MONITOR_ONLY; +} + +static void ShowUnassignedWarning(const char *name) +{ + auto msgBox = [=]() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("VolControl.UnassignedWarning.Title")); + msgbox.setText(QTStr("VolControl.UnassignedWarning.Text").arg(name)); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources", true); + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + } + }; + + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, Q_ARG(VoidFunc, msgBox)); +} + +void VolControl::OBSVolumeChanged(void *data, float db) +{ + Q_UNUSED(db); + VolControl *volControl = static_cast(data); + + QMetaObject::invokeMethod(volControl, "VolumeChanged"); +} + +void VolControl::OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + VolControl *volControl = static_cast(data); + + volControl->volMeter->setLevels(magnitude, peak, inputPeak); +} + +void VolControl::OBSVolumeMuted(void *data, calldata_t *calldata) +{ + VolControl *volControl = static_cast(data); + bool muted = calldata_bool(calldata, "muted"); + + QMetaObject::invokeMethod(volControl, "VolumeMuted", Q_ARG(bool, muted)); +} + +void VolControl::VolumeChanged() +{ + slider->blockSignals(true); + slider->setValue((int)(obs_fader_get_deflection(obs_fader) * FADER_PRECISION)); + slider->blockSignals(false); + + updateText(); +} + +void VolControl::VolumeMuted(bool muted) +{ + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::OBSMixersOrMonitoringChanged(void *data, calldata_t *) +{ + + VolControl *volControl = static_cast(data); + QMetaObject::invokeMethod(volControl, "MixersOrMonitoringChanged", Qt::QueuedConnection); +} + +void VolControl::MixersOrMonitoringChanged() +{ + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + + auto newState = GetCheckState(muted, unassigned); + if (mute->checkState() != newState) + mute->setCheckState(newState); + + volMeter->muted = muted || unassigned; +} + +void VolControl::SetMuted(bool) +{ + bool checked = mute->checkState() == Qt::Checked; + bool prev = obs_source_muted(source); + obs_source_set_muted(source, checked); + bool unassigned = IsSourceUnassigned(source); + + if (!checked && unassigned) { + mute->setCheckState(Qt::PartiallyChecked); + /* Show notice about the source no being assigned to any tracks */ + bool has_shown_warning = + config_get_bool(App()->GetUserConfig(), "General", "WarnedAboutUnassignedSources"); + if (!has_shown_warning) + ShowUnassignedWarning(obs_source_get_name(source)); + } + + auto undo_redo = [](const std::string &uuid, bool val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_muted(source, val); + }; + + QString text = QTStr(checked ? "Undo.Volume.Mute" : "Undo.Volume.Unmute"); + + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(text.arg(name), std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, checked), uuid, uuid); +} + +void VolControl::SliderChanged(int vol) +{ + float prev = obs_source_get_volume(source); + + obs_fader_set_deflection(obs_fader, float(vol) / FADER_PRECISION); + updateText(); + + auto undo_redo = [](const std::string &uuid, float val) { + OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str()); + obs_source_set_volume(source, val); + }; + + float val = obs_source_get_volume(source); + const char *name = obs_source_get_name(source); + const char *uuid = obs_source_get_uuid(source); + OBSBasic::Get()->undo_s.add_action(QTStr("Undo.Volume.Change").arg(name), + std::bind(undo_redo, std::placeholders::_1, prev), + std::bind(undo_redo, std::placeholders::_1, val), uuid, uuid, true); +} + +void VolControl::updateText() +{ + QString text; + float db = obs_fader_get_db(obs_fader); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + volLabel->setText(text); + + bool muted = obs_source_muted(source); + const char *accTextLookup = muted ? "VolControl.SliderMuted" : "VolControl.SliderUnmuted"; + + QString sourceName = obs_source_get_name(source); + QString accText = QTStr(accTextLookup).arg(sourceName); + + slider->setAccessibleName(accText); +} + +void VolControl::EmitConfigClicked() +{ + emit ConfigClicked(); +} + +void VolControl::SetMeterDecayRate(qreal q) +{ + volMeter->setPeakDecayRate(q); +} + +void VolControl::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + volMeter->setPeakMeterType(peakMeterType); +} + +VolControl::VolControl(OBSSource source_, bool showConfig, bool vertical) + : source(std::move(source_)), + levelTotal(0.0f), + levelCount(0.0f), + obs_fader(obs_fader_create(OBS_FADER_LOG)), + obs_volmeter(obs_volmeter_create(OBS_FADER_LOG)), + vertical(vertical), + contextMenu(nullptr) +{ + nameLabel = new OBSSourceLabel(source); + volLabel = new QLabel(); + mute = new MuteCheckBox(); + + volLabel->setObjectName("volLabel"); + volLabel->setAlignment(Qt::AlignCenter); + +#ifdef __APPLE__ + mute->setAttribute(Qt::WA_LayoutUsesWidgetRect); +#endif + + QString sourceName = obs_source_get_name(source); + setObjectName(sourceName); + + if (showConfig) { + config = new QPushButton(this); + config->setProperty("class", "icon-dots-vert"); + config->setAutoDefault(false); + + config->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + config->setAccessibleName(QTStr("VolControl.Properties").arg(sourceName)); + + connect(config, &QAbstractButton::clicked, this, &VolControl::EmitConfigClicked); + } + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + if (vertical) { + QHBoxLayout *nameLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QHBoxLayout *volLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QHBoxLayout *meterLayout = new QHBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, true); + slider = new VolumeSlider(obs_fader, Qt::Vertical); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + nameLayout->setAlignment(Qt::AlignCenter); + meterLayout->setAlignment(Qt::AlignCenter); + controlLayout->setAlignment(Qt::AlignCenter); + volLayout->setAlignment(Qt::AlignCenter); + + meterFrame->setObjectName("volMeterFrame"); + + nameLayout->setContentsMargins(0, 0, 0, 0); + nameLayout->setSpacing(0); + nameLayout->addWidget(nameLabel); + + controlLayout->setContentsMargins(0, 0, 0, 0); + controlLayout->setSpacing(0); + + // Add Headphone (audio monitoring) widget here + controlLayout->addWidget(mute); + + if (showConfig) { + controlLayout->addWidget(config); + } + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + meterLayout->addWidget(slider); + meterLayout->addWidget(volMeter); + + meterFrame->setLayout(meterLayout); + + volLayout->setContentsMargins(0, 0, 0, 0); + volLayout->setSpacing(0); + volLayout->addWidget(volLabel); + volLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::Minimum)); + + mainLayout->addItem(nameLayout); + mainLayout->addItem(volLayout); + mainLayout->addWidget(meterFrame); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + + // Default size can cause clipping of long names in vertical layout. + QFont font = nameLabel->font(); + QFontInfo info(font); + nameLabel->setFont(font); + + setMaximumWidth(110); + } else { + QHBoxLayout *textLayout = new QHBoxLayout; + QHBoxLayout *controlLayout = new QHBoxLayout; + QFrame *meterFrame = new QFrame; + QVBoxLayout *meterLayout = new QVBoxLayout; + QVBoxLayout *buttonLayout = new QVBoxLayout; + + volMeter = new VolumeMeter(nullptr, obs_volmeter, false); + volMeter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + + slider = new VolumeSlider(obs_fader, Qt::Horizontal); + slider->setLayoutDirection(Qt::LeftToRight); + slider->setDisplayTicks(true); + + textLayout->setContentsMargins(0, 0, 0, 0); + textLayout->addWidget(nameLabel); + textLayout->addWidget(volLabel); + textLayout->setAlignment(nameLabel, Qt::AlignLeft); + textLayout->setAlignment(volLabel, Qt::AlignRight); + + meterFrame->setObjectName("volMeterFrame"); + meterFrame->setLayout(meterLayout); + + meterLayout->setContentsMargins(0, 0, 0, 0); + meterLayout->setSpacing(0); + + meterLayout->addWidget(volMeter); + meterLayout->addWidget(slider); + + buttonLayout->setContentsMargins(0, 0, 0, 0); + buttonLayout->setSpacing(0); + + if (showConfig) { + buttonLayout->addWidget(config); + } + buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding)); + buttonLayout->addWidget(mute); + + controlLayout->addItem(buttonLayout); + controlLayout->addWidget(meterFrame); + + mainLayout->addItem(textLayout); + mainLayout->addItem(controlLayout); + + volMeter->setFocusProxy(slider); + } + + setLayout(mainLayout); + + nameLabel->setText(sourceName); + + slider->setMinimum(0); + slider->setMaximum(int(FADER_PRECISION)); + + bool muted = obs_source_muted(source); + bool unassigned = IsSourceUnassigned(source); + mute->setCheckState(GetCheckState(muted, unassigned)); + volMeter->muted = muted || unassigned; + mute->setAccessibleName(QTStr("VolControl.Mute").arg(sourceName)); + obs_fader_add_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_add_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.emplace_back(obs_source_get_signal_handler(source), "mute", OBSVolumeMuted, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_mixers", OBSMixersOrMonitoringChanged, this); + sigs.emplace_back(obs_source_get_signal_handler(source), "audio_monitoring", OBSMixersOrMonitoringChanged, + this); + + QWidget::connect(slider, &VolumeSlider::valueChanged, this, &VolControl::SliderChanged); + QWidget::connect(mute, &MuteCheckBox::clicked, this, &VolControl::SetMuted); + + obs_fader_attach_source(obs_fader, source); + obs_volmeter_attach_source(obs_volmeter, source); + + /* Call volume changed once to init the slider position and label */ + VolumeChanged(); +} + +void VolControl::EnableSlider(bool enable) +{ + slider->setEnabled(enable); +} + +VolControl::~VolControl() +{ + obs_fader_remove_callback(obs_fader, OBSVolumeChanged, this); + obs_volmeter_remove_callback(obs_volmeter, OBSVolumeLevel, this); + + sigs.clear(); + + if (contextMenu) + contextMenu->close(); +} + +static inline QColor color_from_int(long long val) +{ + QColor color(val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff); + color.setAlpha(255); + + return color; +} + +QColor VolumeMeter::getBackgroundNominalColor() const +{ + return p_backgroundNominalColor; +} + +QColor VolumeMeter::getBackgroundNominalColorDisabled() const +{ + return backgroundNominalColorDisabled; +} + +void VolumeMeter::setBackgroundNominalColor(QColor c) +{ + p_backgroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreen")); + } else { + backgroundNominalColor = p_backgroundNominalColor; + } +} + +void VolumeMeter::setBackgroundNominalColorDisabled(QColor c) +{ + backgroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundWarningColor() const +{ + return p_backgroundWarningColor; +} + +QColor VolumeMeter::getBackgroundWarningColorDisabled() const +{ + return backgroundWarningColorDisabled; +} + +void VolumeMeter::setBackgroundWarningColor(QColor c) +{ + p_backgroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellow")); + } else { + backgroundWarningColor = p_backgroundWarningColor; + } +} + +void VolumeMeter::setBackgroundWarningColorDisabled(QColor c) +{ + backgroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getBackgroundErrorColor() const +{ + return p_backgroundErrorColor; +} + +QColor VolumeMeter::getBackgroundErrorColorDisabled() const +{ + return backgroundErrorColorDisabled; +} + +void VolumeMeter::setBackgroundErrorColor(QColor c) +{ + p_backgroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + backgroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRed")); + } else { + backgroundErrorColor = p_backgroundErrorColor; + } +} + +void VolumeMeter::setBackgroundErrorColorDisabled(QColor c) +{ + backgroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundNominalColor() const +{ + return p_foregroundNominalColor; +} + +QColor VolumeMeter::getForegroundNominalColorDisabled() const +{ + return foregroundNominalColorDisabled; +} + +void VolumeMeter::setForegroundNominalColor(QColor c) +{ + p_foregroundNominalColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundNominalColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerGreenActive")); + } else { + foregroundNominalColor = p_foregroundNominalColor; + } +} + +void VolumeMeter::setForegroundNominalColorDisabled(QColor c) +{ + foregroundNominalColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundWarningColor() const +{ + return p_foregroundWarningColor; +} + +QColor VolumeMeter::getForegroundWarningColorDisabled() const +{ + return foregroundWarningColorDisabled; +} + +void VolumeMeter::setForegroundWarningColor(QColor c) +{ + p_foregroundWarningColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundWarningColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerYellowActive")); + } else { + foregroundWarningColor = p_foregroundWarningColor; + } +} + +void VolumeMeter::setForegroundWarningColorDisabled(QColor c) +{ + foregroundWarningColorDisabled = std::move(c); +} + +QColor VolumeMeter::getForegroundErrorColor() const +{ + return p_foregroundErrorColor; +} + +QColor VolumeMeter::getForegroundErrorColorDisabled() const +{ + return foregroundErrorColorDisabled; +} + +void VolumeMeter::setForegroundErrorColor(QColor c) +{ + p_foregroundErrorColor = std::move(c); + + if (config_get_bool(App()->GetUserConfig(), "Accessibility", "OverrideColors")) { + foregroundErrorColor = + color_from_int(config_get_int(App()->GetUserConfig(), "Accessibility", "MixerRedActive")); + } else { + foregroundErrorColor = p_foregroundErrorColor; + } +} + +void VolumeMeter::setForegroundErrorColorDisabled(QColor c) +{ + foregroundErrorColorDisabled = std::move(c); +} + +QColor VolumeMeter::getClipColor() const +{ + return clipColor; +} + +void VolumeMeter::setClipColor(QColor c) +{ + clipColor = std::move(c); +} + +QColor VolumeMeter::getMagnitudeColor() const +{ + return magnitudeColor; +} + +void VolumeMeter::setMagnitudeColor(QColor c) +{ + magnitudeColor = std::move(c); +} + +QColor VolumeMeter::getMajorTickColor() const +{ + return majorTickColor; +} + +void VolumeMeter::setMajorTickColor(QColor c) +{ + majorTickColor = std::move(c); +} + +QColor VolumeMeter::getMinorTickColor() const +{ + return minorTickColor; +} + +void VolumeMeter::setMinorTickColor(QColor c) +{ + minorTickColor = std::move(c); +} + +int VolumeMeter::getMeterThickness() const +{ + return meterThickness; +} + +void VolumeMeter::setMeterThickness(int v) +{ + meterThickness = v; + recalculateLayout = true; +} + +qreal VolumeMeter::getMeterFontScaling() const +{ + return meterFontScaling; +} + +void VolumeMeter::setMeterFontScaling(qreal v) +{ + meterFontScaling = v; + recalculateLayout = true; +} + +void VolControl::refreshColors() +{ + volMeter->setBackgroundNominalColor(volMeter->getBackgroundNominalColor()); + volMeter->setBackgroundWarningColor(volMeter->getBackgroundWarningColor()); + volMeter->setBackgroundErrorColor(volMeter->getBackgroundErrorColor()); + volMeter->setForegroundNominalColor(volMeter->getForegroundNominalColor()); + volMeter->setForegroundWarningColor(volMeter->getForegroundWarningColor()); + volMeter->setForegroundErrorColor(volMeter->getForegroundErrorColor()); +} + +qreal VolumeMeter::getMinimumLevel() const +{ + return minimumLevel; +} + +void VolumeMeter::setMinimumLevel(qreal v) +{ + minimumLevel = v; +} + +qreal VolumeMeter::getWarningLevel() const +{ + return warningLevel; +} + +void VolumeMeter::setWarningLevel(qreal v) +{ + warningLevel = v; +} + +qreal VolumeMeter::getErrorLevel() const +{ + return errorLevel; +} + +void VolumeMeter::setErrorLevel(qreal v) +{ + errorLevel = v; +} + +qreal VolumeMeter::getClipLevel() const +{ + return clipLevel; +} + +void VolumeMeter::setClipLevel(qreal v) +{ + clipLevel = v; +} + +qreal VolumeMeter::getMinimumInputLevel() const +{ + return minimumInputLevel; +} + +void VolumeMeter::setMinimumInputLevel(qreal v) +{ + minimumInputLevel = v; +} + +qreal VolumeMeter::getPeakDecayRate() const +{ + return peakDecayRate; +} + +void VolumeMeter::setPeakDecayRate(qreal v) +{ + peakDecayRate = v; +} + +qreal VolumeMeter::getMagnitudeIntegrationTime() const +{ + return magnitudeIntegrationTime; +} + +void VolumeMeter::setMagnitudeIntegrationTime(qreal v) +{ + magnitudeIntegrationTime = v; +} + +qreal VolumeMeter::getPeakHoldDuration() const +{ + return peakHoldDuration; +} + +void VolumeMeter::setPeakHoldDuration(qreal v) +{ + peakHoldDuration = v; +} + +qreal VolumeMeter::getInputPeakHoldDuration() const +{ + return inputPeakHoldDuration; +} + +void VolumeMeter::setInputPeakHoldDuration(qreal v) +{ + inputPeakHoldDuration = v; +} + +void VolumeMeter::setPeakMeterType(enum obs_peak_meter_type peakMeterType) +{ + obs_volmeter_set_peak_meter_type(obs_volmeter, peakMeterType); + switch (peakMeterType) { + case TRUE_PEAK_METER: + // For true-peak meters EBU has defined the Permitted Maximum, + // taking into account the accuracy of the meter and further + // processing required by lossy audio compression. + // + // The alignment level was not specified, but I've adjusted + // it compared to a sample-peak meter. Incidentally Youtube + // uses this new Alignment Level as the maximum integrated + // loudness of a video. + // + // * Permitted Maximum Level (PML) = -2.0 dBTP + // * Alignment Level (AL) = -13 dBTP + setErrorLevel(-2.0); + setWarningLevel(-13.0); + break; + + case SAMPLE_PEAK_METER: + default: + // For a sample Peak Meter EBU has the following level + // definitions, taking into account inaccuracies of this meter: + // + // * Permitted Maximum Level (PML) = -9.0 dBFS + // * Alignment Level (AL) = -20.0 dBFS + setErrorLevel(-9.0); + setWarningLevel(-20.0); + break; + } +} + +void VolumeMeter::mousePressEvent(QMouseEvent *event) +{ + setFocus(Qt::MouseFocusReason); + event->accept(); +} + +void VolumeMeter::wheelEvent(QWheelEvent *event) +{ + QApplication::sendEvent(focusProxy(), event); +} + +VolumeMeter::VolumeMeter(QWidget *parent, obs_volmeter_t *obs_volmeter, bool vertical) + : QWidget(parent), + obs_volmeter(obs_volmeter), + vertical(vertical) +{ + setAttribute(Qt::WA_OpaquePaintEvent, true); + + // Default meter settings, they only show if + // there is no stylesheet, do not remove. + backgroundNominalColor.setRgb(0x26, 0x7f, 0x26); // Dark green + backgroundWarningColor.setRgb(0x7f, 0x7f, 0x26); // Dark yellow + backgroundErrorColor.setRgb(0x7f, 0x26, 0x26); // Dark red + foregroundNominalColor.setRgb(0x4c, 0xff, 0x4c); // Bright green + foregroundWarningColor.setRgb(0xff, 0xff, 0x4c); // Bright yellow + foregroundErrorColor.setRgb(0xff, 0x4c, 0x4c); // Bright red + + backgroundNominalColorDisabled.setRgb(90, 90, 90); + backgroundWarningColorDisabled.setRgb(117, 117, 117); + backgroundErrorColorDisabled.setRgb(65, 65, 65); + foregroundNominalColorDisabled.setRgb(163, 163, 163); + foregroundWarningColorDisabled.setRgb(217, 217, 217); + foregroundErrorColorDisabled.setRgb(113, 113, 113); + + clipColor.setRgb(0xff, 0xff, 0xff); // Bright white + magnitudeColor.setRgb(0x00, 0x00, 0x00); // Black + majorTickColor.setRgb(0x00, 0x00, 0x00); // Black + minorTickColor.setRgb(0x32, 0x32, 0x32); // Dark gray + minimumLevel = -60.0; // -60 dB + warningLevel = -20.0; // -20 dB + errorLevel = -9.0; // -9 dB + clipLevel = -0.5; // -0.5 dB + minimumInputLevel = -50.0; // -50 dB + peakDecayRate = 11.76; // 20 dB / 1.7 sec + magnitudeIntegrationTime = 0.3; // 99% in 300 ms + peakHoldDuration = 20.0; // 20 seconds + inputPeakHoldDuration = 1.0; // 1 second + meterThickness = 3; // Bar thickness in pixels + meterFontScaling = 0.7; // Font size for numbers is 70% of Widget's font size + channels = (int)audio_output_get_channels(obs_get_audio()); + + doLayout(); + updateTimerRef = updateTimer.lock(); + if (!updateTimerRef) { + updateTimerRef = std::make_shared(); + updateTimerRef->setTimerType(Qt::PreciseTimer); + updateTimerRef->start(16); + updateTimer = updateTimerRef; + } + + updateTimerRef->AddVolControl(this); +} + +VolumeMeter::~VolumeMeter() +{ + updateTimerRef->RemoveVolControl(this); +} + +void VolumeMeter::setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]) +{ + uint64_t ts = os_gettime_ns(); + QMutexLocker locker(&dataMutex); + + currentLastUpdateTime = ts; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = magnitude[channelNr]; + currentPeak[channelNr] = peak[channelNr]; + currentInputPeak[channelNr] = inputPeak[channelNr]; + } + + // In case there are more updates then redraws we must make sure + // that the ballistics of peak and hold are recalculated. + locker.unlock(); + calculateBallistics(ts); +} + +inline void VolumeMeter::resetLevels() +{ + currentLastUpdateTime = 0; + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) { + currentMagnitude[channelNr] = -M_INFINITE; + currentPeak[channelNr] = -M_INFINITE; + currentInputPeak[channelNr] = -M_INFINITE; + + displayMagnitude[channelNr] = -M_INFINITE; + displayPeak[channelNr] = -M_INFINITE; + displayPeakHold[channelNr] = -M_INFINITE; + displayPeakHoldLastUpdateTime[channelNr] = 0; + displayInputPeakHold[channelNr] = -M_INFINITE; + displayInputPeakHoldLastUpdateTime[channelNr] = 0; + } +} + +bool VolumeMeter::needLayoutChange() +{ + int currentNrAudioChannels = obs_volmeter_get_nr_channels(obs_volmeter); + + if (!currentNrAudioChannels) { + struct obs_audio_info oai; + obs_get_audio_info(&oai); + currentNrAudioChannels = (oai.speakers == SPEAKERS_MONO) ? 1 : 2; + } + + if (displayNrAudioChannels != currentNrAudioChannels) { + displayNrAudioChannels = currentNrAudioChannels; + recalculateLayout = true; + } + + return recalculateLayout; +} + +// When this is called from the constructor, obs_volmeter_get_nr_channels has not +// yet been called and Q_PROPERTY settings have not yet been read from the +// stylesheet. +inline void VolumeMeter::doLayout() +{ + QMutexLocker locker(&dataMutex); + + if (displayNrAudioChannels) { + int meterSize = std::floor(22 / displayNrAudioChannels); + setMeterThickness(std::clamp(meterSize, 3, 7)); + } + recalculateLayout = false; + + tickFont = font(); + QFontInfo info(tickFont); + tickFont.setPointSizeF(info.pointSizeF() * meterFontScaling); + QFontMetrics metrics(tickFont); + if (vertical) { + // Each meter channel is meterThickness pixels wide, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, space to hold our longest label in this font, + // and a few pixels before the fader. + QRect scaleBounds = metrics.boundingRect("-88"); + setMinimumSize(displayNrAudioChannels * (meterThickness + 1) - 1 + 10 + scaleBounds.width() + 2, 100); + } else { + // Each meter channel is meterThickness pixels high, plus one pixel + // between channels, but not after the last. + // Add 4 pixels for ticks, and space high enough to hold our label in + // this font, presuming that digits don't have descenders. + setMinimumSize(100, displayNrAudioChannels * (meterThickness + 1) - 1 + 4 + metrics.capHeight()); + } + + resetLevels(); +} + +inline bool VolumeMeter::detectIdle(uint64_t ts) +{ + double timeSinceLastUpdate = (ts - currentLastUpdateTime) * 0.000000001; + if (timeSinceLastUpdate > 0.5) { + resetLevels(); + return true; + } else { + return false; + } +} + +inline void VolumeMeter::calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw) +{ + if (currentPeak[channelNr] >= displayPeak[channelNr] || isnan(displayPeak[channelNr])) { + // Attack of peak is immediate. + displayPeak[channelNr] = currentPeak[channelNr]; + } else { + // Decay of peak is 40 dB / 1.7 seconds for Fast Profile + // 20 dB / 1.7 seconds for Medium Profile (Type I PPM) + // 24 dB / 2.8 seconds for Slow Profile (Type II PPM) + float decay = float(peakDecayRate * timeSinceLastRedraw); + displayPeak[channelNr] = + std::clamp(displayPeak[channelNr] - decay, std::min(currentPeak[channelNr], 0.f), 0.f); + } + + if (currentPeak[channelNr] >= displayPeakHold[channelNr] || !isfinite(displayPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak + // after 20 seconds. + qreal timeSinceLastPeak = (uint64_t)(ts - displayPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > peakHoldDuration) { + displayPeakHold[channelNr] = currentPeak[channelNr]; + displayPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (currentInputPeak[channelNr] >= displayInputPeakHold[channelNr] || + !isfinite(displayInputPeakHold[channelNr])) { + // Attack of peak-hold is immediate, but keep track + // when it was last updated. + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } else { + // The peak and hold falls back to peak after 1 second. + qreal timeSinceLastPeak = (uint64_t)(ts - displayInputPeakHoldLastUpdateTime[channelNr]) * 0.000000001; + if (timeSinceLastPeak > inputPeakHoldDuration) { + displayInputPeakHold[channelNr] = currentInputPeak[channelNr]; + displayInputPeakHoldLastUpdateTime[channelNr] = ts; + } + } + + if (!isfinite(displayMagnitude[channelNr])) { + // The statements in the else-leg do not work with + // NaN and infinite displayMagnitude. + displayMagnitude[channelNr] = currentMagnitude[channelNr]; + } else { + // A VU meter will integrate to the new value to 99% in 300 ms. + // The calculation here is very simplified and is more accurate + // with higher frame-rate. + float attack = float((currentMagnitude[channelNr] - displayMagnitude[channelNr]) * + (timeSinceLastRedraw / magnitudeIntegrationTime) * 0.99); + displayMagnitude[channelNr] = + std::clamp(displayMagnitude[channelNr] + attack, (float)minimumLevel, 0.f); + } +} + +inline void VolumeMeter::calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw) +{ + QMutexLocker locker(&dataMutex); + + for (int channelNr = 0; channelNr < MAX_AUDIO_CHANNELS; channelNr++) + calculateBallisticsForChannel(channelNr, ts, timeSinceLastRedraw); +} + +void VolumeMeter::paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold) +{ + QMutexLocker locker(&dataMutex); + QColor color; + + if (peakHold < minimumInputLevel) + color = backgroundNominalColor; + else if (peakHold < warningLevel) + color = foregroundNominalColor; + else if (peakHold < errorLevel) + color = foregroundWarningColor; + else if (peakHold <= clipLevel) + color = foregroundErrorColor; + else + color = clipColor; + + painter.fillRect(x, y, width, height, color); +} + +void VolumeMeter::paintHTicks(QPainter &painter, int x, int y, int width) +{ + qreal scale = width / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = int(x + width - (i * scale) - 1); + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + QRect textBounds = metrics.boundingRect(str); + int pos; + if (i == 0) { + pos = position - textBounds.width(); + } else { + pos = position - (textBounds.width() / 2); + if (pos < 0) + pos = 0; + } + painter.drawText(pos, y + 4 + metrics.capHeight(), str); + + painter.drawLine(position, y, position, y + 2); + } +} + +void VolumeMeter::paintVTicks(QPainter &painter, int x, int y, int height) +{ + qreal scale = height / minimumLevel; + + painter.setFont(tickFont); + QFontMetrics metrics(tickFont); + painter.setPen(majorTickColor); + + // Draw major tick lines and numeric indicators. + for (int i = 0; i >= minimumLevel; i -= 5) { + int position = y + int(i * scale) + METER_PADDING; + QString str = QString::number(i); + + // Center the number on the tick, but don't overflow + if (i == 0) { + painter.drawText(x + 10, position + metrics.capHeight(), str); + } else { + painter.drawText(x + 8, position + (metrics.capHeight() / 2), str); + } + + painter.drawLine(x, position, x + 2, position); + } +} + +#define CLIP_FLASH_DURATION_MS 1000 + +inline int VolumeMeter::convertToInt(float number) +{ + constexpr int min = std::numeric_limits::min(); + constexpr int max = std::numeric_limits::max(); + + // NOTE: Conversion from 'const int' to 'float' changes max value from 2147483647 to 2147483648 + if (number >= (float)max) + return max; + else if (number < min) + return min; + else + return int(number); +} + +void VolumeMeter::paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = width / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = x + 0; + int maximumPosition = x + width; + int magnitudePosition = x + width - convertToInt(magnitude * scale); + int peakPosition = x + width - convertToInt(peak * scale); + int peakHoldPosition = x + width - convertToInt(peakHold * scale); + int warningPosition = x + width - convertToInt(warningLevel * scale); + int errorPosition = x + width - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(minimumPosition, y, peakPosition - minimumPosition, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(peakPosition, y, warningPosition - peakPosition, height, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, peakPosition - warningPosition, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(peakPosition, y, errorPosition - peakPosition, height, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(errorPosition, y, errorLength, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(minimumPosition, y, nominalLength, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(warningPosition, y, warningLength, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(errorPosition, y, peakPosition - errorPosition, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(peakPosition, y, maximumPosition - peakPosition, height, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(minimumPosition, y, end, height, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(peakHoldPosition - 3, y, 3, height, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(magnitudePosition - 3, y, 3, height, magnitudeColor); +} + +void VolumeMeter::paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold) +{ + qreal scale = height / minimumLevel; + + QMutexLocker locker(&dataMutex); + int minimumPosition = y + 0; + int maximumPosition = y + height; + int magnitudePosition = y + height - convertToInt(magnitude * scale); + int peakPosition = y + height - convertToInt(peak * scale); + int peakHoldPosition = y + height - convertToInt(peakHold * scale); + int warningPosition = y + height - convertToInt(warningLevel * scale); + int errorPosition = y + height - convertToInt(errorLevel * scale); + + int nominalLength = warningPosition - minimumPosition; + int warningLength = errorPosition - warningPosition; + int errorLength = maximumPosition - errorPosition; + locker.unlock(); + + if (clipping) { + peakPosition = maximumPosition; + } + + if (peakPosition < minimumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < warningPosition) { + painter.fillRect(x, minimumPosition, width, peakPosition - minimumPosition, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, peakPosition, width, warningPosition - peakPosition, + muted ? backgroundNominalColorDisabled : backgroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < errorPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, peakPosition - warningPosition, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, peakPosition, width, errorPosition - peakPosition, + muted ? backgroundWarningColorDisabled : backgroundWarningColor); + painter.fillRect(x, errorPosition, width, errorLength, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else if (peakPosition < maximumPosition) { + painter.fillRect(x, minimumPosition, width, nominalLength, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + painter.fillRect(x, warningPosition, width, warningLength, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + painter.fillRect(x, errorPosition, width, peakPosition - errorPosition, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + painter.fillRect(x, peakPosition, width, maximumPosition - peakPosition, + muted ? backgroundErrorColorDisabled : backgroundErrorColor); + } else { + if (!clipping) { + QTimer::singleShot(CLIP_FLASH_DURATION_MS, this, [&]() { clipping = false; }); + clipping = true; + } + + int end = errorLength + warningLength + nominalLength; + painter.fillRect(x, minimumPosition, width, end, + QBrush(muted ? foregroundErrorColorDisabled : foregroundErrorColor)); + } + + if (peakHoldPosition - 3 < minimumPosition) + ; // Peak-hold below minimum, no drawing. + else if (peakHoldPosition < warningPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundNominalColorDisabled : foregroundNominalColor); + else if (peakHoldPosition < errorPosition) + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundWarningColorDisabled : foregroundWarningColor); + else + painter.fillRect(x, peakHoldPosition - 3, width, 3, + muted ? foregroundErrorColorDisabled : foregroundErrorColor); + + if (magnitudePosition - 3 >= minimumPosition) + painter.fillRect(x, magnitudePosition - 3, width, 3, magnitudeColor); +} + +void VolumeMeter::paintEvent(QPaintEvent *event) +{ + uint64_t ts = os_gettime_ns(); + qreal timeSinceLastRedraw = (ts - lastRedrawTime) * 0.000000001; + calculateBallistics(ts, timeSinceLastRedraw); + bool idle = detectIdle(ts); + + QRect widgetRect = rect(); + int width = widgetRect.width(); + int height = widgetRect.height(); + + QPainter painter(this); + + // Paint window background color (as widget is opaque) + QColor background = palette().color(QPalette::ColorRole::Window); + painter.fillRect(event->region().boundingRect(), background); + + if (vertical) + height -= METER_PADDING * 2; + + // timerEvent requests update of the bar(s) only, so we can avoid the + // overhead of repainting the scale and labels. + if (event->region().boundingRect() != getBarRect()) { + if (needLayoutChange()) + doLayout(); + + if (vertical) { + paintVTicks(painter, displayNrAudioChannels * (meterThickness + 1) - 1, 0, + height - (INDICATOR_THICKNESS + 3)); + } else { + paintHTicks(painter, INDICATOR_THICKNESS + 3, displayNrAudioChannels * (meterThickness + 1) - 1, + width - (INDICATOR_THICKNESS + 3)); + } + } + + if (vertical) { + // Invert the Y axis to ease the math + painter.translate(0, height + METER_PADDING); + painter.scale(1, -1); + } + + for (int channelNr = 0; channelNr < displayNrAudioChannels; channelNr++) { + + int channelNrFixed = (displayNrAudioChannels == 1 && channels > 2) ? 2 : channelNr; + + if (vertical) + paintVMeter(painter, channelNr * (meterThickness + 1), INDICATOR_THICKNESS + 2, meterThickness, + height - (INDICATOR_THICKNESS + 2), displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + else + paintHMeter(painter, INDICATOR_THICKNESS + 2, channelNr * (meterThickness + 1), + width - (INDICATOR_THICKNESS + 2), meterThickness, displayMagnitude[channelNrFixed], + displayPeak[channelNrFixed], displayPeakHold[channelNrFixed]); + + if (idle) + continue; + + // By not drawing the input meter boxes the user can + // see that the audio stream has been stopped, without + // having too much visual impact. + if (vertical) + paintInputMeter(painter, channelNr * (meterThickness + 1), 0, meterThickness, + INDICATOR_THICKNESS, displayInputPeakHold[channelNrFixed]); + else + paintInputMeter(painter, 0, channelNr * (meterThickness + 1), INDICATOR_THICKNESS, + meterThickness, displayInputPeakHold[channelNrFixed]); + } + + lastRedrawTime = ts; +} + +QRect VolumeMeter::getBarRect() const +{ + QRect rec = rect(); + if (vertical) + rec.setWidth(displayNrAudioChannels * (meterThickness + 1) - 1); + else + rec.setHeight(displayNrAudioChannels * (meterThickness + 1) - 1); + + return rec; +} + +void VolumeMeter::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::StyleChange) + recalculateLayout = true; + + QWidget::changeEvent(e); +} + +void VolumeMeterTimer::AddVolControl(VolumeMeter *meter) +{ + volumeMeters.push_back(meter); +} + +void VolumeMeterTimer::RemoveVolControl(VolumeMeter *meter) +{ + volumeMeters.removeOne(meter); +} + +void VolumeMeterTimer::timerEvent(QTimerEvent *) +{ + for (VolumeMeter *meter : volumeMeters) { + if (meter->needLayoutChange()) { + // Tell paintEvent to update layout and paint everything + meter->update(); + } else { + // Tell paintEvent to paint only the bars + meter->update(meter->getBarRect()); + } + } +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, QWidget *parent) : AbsoluteSlider(parent) +{ + fad = fader; +} + +VolumeSlider::VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent) + : AbsoluteSlider(orientation, parent) +{ + fad = fader; +} + +bool VolumeSlider::getDisplayTicks() const +{ + return displayTicks; +} + +void VolumeSlider::setDisplayTicks(bool display) +{ + displayTicks = display; +} + +void VolumeSlider::paintEvent(QPaintEvent *event) +{ + if (!getDisplayTicks()) { + QSlider::paintEvent(event); + return; + } + + QPainter painter(this); + QColor tickColor(91, 98, 115, 255); + + obs_fader_conversion_t fader_db_to_def = obs_fader_db_to_def(fad); + + QStyleOptionSlider opt; + initStyleOption(&opt); + + QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); + QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); + + if (orientation() == Qt::Horizontal) { + const int sliderWidth = groove.width() - handle.width(); + + float tickLength = groove.height() * 1.5; + tickLength = std::max((int)tickLength + groove.height(), 8 + groove.height()); + + float yPos = groove.center().y() - (tickLength / 2) + 1; + + for (int db = -10; db >= -90; db -= 10) { + float tickValue = fader_db_to_def(db); + + float xPos = groove.left() + (tickValue * sliderWidth) + (handle.width() / 2); + painter.fillRect(xPos, yPos, 1, tickLength, tickColor); + } + } + + if (orientation() == Qt::Vertical) { + const int sliderHeight = groove.height() - handle.height(); + + float tickLength = groove.width() * 1.5; + tickLength = std::max((int)tickLength + groove.width(), 8 + groove.width()); + + float xPos = groove.center().x() - (tickLength / 2) + 1; + + for (int db = -10; db >= -96; db -= 10) { + float tickValue = fader_db_to_def(db); + + float yPos = + groove.height() + groove.top() - (tickValue * sliderHeight) - (handle.height() / 2); + painter.fillRect(xPos, yPos, tickLength, 1, tickColor); + } + } + + QSlider::paintEvent(event); +} + +VolumeAccessibleInterface::VolumeAccessibleInterface(QWidget *w) : QAccessibleWidget(w) {} + +VolumeSlider *VolumeAccessibleInterface::slider() const +{ + return qobject_cast(object()); +} + +QString VolumeAccessibleInterface::text(QAccessible::Text t) const +{ + if (slider()->isVisible()) { + switch (t) { + case QAccessible::Text::Value: + return currentValue().toString(); + default: + break; + } + } + return QAccessibleWidget::text(t); +} + +QVariant VolumeAccessibleInterface::currentValue() const +{ + QString text; + float db = obs_fader_get_db(slider()->fad); + + if (db < -96.0f) + text = "-inf dB"; + else + text = QString::number(db, 'f', 1).append(" dB"); + + return text; +} + +void VolumeAccessibleInterface::setCurrentValue(const QVariant &value) +{ + slider()->setValue(value.toInt()); +} + +QVariant VolumeAccessibleInterface::maximumValue() const +{ + return slider()->maximum(); +} + +QVariant VolumeAccessibleInterface::minimumValue() const +{ + return slider()->minimum(); +} + +QVariant VolumeAccessibleInterface::minimumStepSize() const +{ + return slider()->singleStep(); +} + +QAccessible::Role VolumeAccessibleInterface::role() const +{ + return QAccessible::Role::Slider; +} diff --git a/frontend/widgets/VolumeMeter.hpp b/frontend/widgets/VolumeMeter.hpp new file mode 100644 index 000000000..51c77f247 --- /dev/null +++ b/frontend/widgets/VolumeMeter.hpp @@ -0,0 +1,341 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "absolute-slider.hpp" + +class QPushButton; +class VolumeMeterTimer; +class VolumeSlider; + +class VolumeMeter : public QWidget { + Q_OBJECT + Q_PROPERTY(QColor backgroundNominalColor READ getBackgroundNominalColor WRITE setBackgroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColor READ getBackgroundWarningColor WRITE setBackgroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor backgroundErrorColor READ getBackgroundErrorColor WRITE setBackgroundErrorColor DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColor READ getForegroundNominalColor WRITE setForegroundNominalColor + DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColor READ getForegroundWarningColor WRITE setForegroundWarningColor + DESIGNABLE true) + Q_PROPERTY( + QColor foregroundErrorColor READ getForegroundErrorColor WRITE setForegroundErrorColor DESIGNABLE true) + + Q_PROPERTY(QColor backgroundNominalColorDisabled READ getBackgroundNominalColorDisabled WRITE + setBackgroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundWarningColorDisabled READ getBackgroundWarningColorDisabled WRITE + setBackgroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor backgroundErrorColorDisabled READ getBackgroundErrorColorDisabled WRITE + setBackgroundErrorColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundNominalColorDisabled READ getForegroundNominalColorDisabled WRITE + setForegroundNominalColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundWarningColorDisabled READ getForegroundWarningColorDisabled WRITE + setForegroundWarningColorDisabled DESIGNABLE true) + Q_PROPERTY(QColor foregroundErrorColorDisabled READ getForegroundErrorColorDisabled WRITE + setForegroundErrorColorDisabled DESIGNABLE true) + + Q_PROPERTY(QColor clipColor READ getClipColor WRITE setClipColor DESIGNABLE true) + Q_PROPERTY(QColor magnitudeColor READ getMagnitudeColor WRITE setMagnitudeColor DESIGNABLE true) + Q_PROPERTY(QColor majorTickColor READ getMajorTickColor WRITE setMajorTickColor DESIGNABLE true) + Q_PROPERTY(QColor minorTickColor READ getMinorTickColor WRITE setMinorTickColor DESIGNABLE true) + Q_PROPERTY(int meterThickness READ getMeterThickness WRITE setMeterThickness DESIGNABLE true) + Q_PROPERTY(qreal meterFontScaling READ getMeterFontScaling WRITE setMeterFontScaling DESIGNABLE true) + + // Levels are denoted in dBFS. + Q_PROPERTY(qreal minimumLevel READ getMinimumLevel WRITE setMinimumLevel DESIGNABLE true) + Q_PROPERTY(qreal warningLevel READ getWarningLevel WRITE setWarningLevel DESIGNABLE true) + Q_PROPERTY(qreal errorLevel READ getErrorLevel WRITE setErrorLevel DESIGNABLE true) + Q_PROPERTY(qreal clipLevel READ getClipLevel WRITE setClipLevel DESIGNABLE true) + Q_PROPERTY(qreal minimumInputLevel READ getMinimumInputLevel WRITE setMinimumInputLevel DESIGNABLE true) + + // Rates are denoted in dB/second. + Q_PROPERTY(qreal peakDecayRate READ getPeakDecayRate WRITE setPeakDecayRate DESIGNABLE true) + + // Time in seconds for the VU meter to integrate over. + Q_PROPERTY(qreal magnitudeIntegrationTime READ getMagnitudeIntegrationTime WRITE setMagnitudeIntegrationTime + DESIGNABLE true) + + // Duration is denoted in seconds. + Q_PROPERTY(qreal peakHoldDuration READ getPeakHoldDuration WRITE setPeakHoldDuration DESIGNABLE true) + Q_PROPERTY(qreal inputPeakHoldDuration READ getInputPeakHoldDuration WRITE setInputPeakHoldDuration + DESIGNABLE true) + + friend class VolControl; + +private: + obs_volmeter_t *obs_volmeter; + static std::weak_ptr updateTimer; + std::shared_ptr updateTimerRef; + + inline void resetLevels(); + inline void doLayout(); + inline bool detectIdle(uint64_t ts); + inline void calculateBallistics(uint64_t ts, qreal timeSinceLastRedraw = 0.0); + inline void calculateBallisticsForChannel(int channelNr, uint64_t ts, qreal timeSinceLastRedraw); + + inline int convertToInt(float number); + void paintInputMeter(QPainter &painter, int x, int y, int width, int height, float peakHold); + void paintHMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintHTicks(QPainter &painter, int x, int y, int width); + void paintVMeter(QPainter &painter, int x, int y, int width, int height, float magnitude, float peak, + float peakHold); + void paintVTicks(QPainter &painter, int x, int y, int height); + + QMutex dataMutex; + + bool recalculateLayout = true; + uint64_t currentLastUpdateTime = 0; + float currentMagnitude[MAX_AUDIO_CHANNELS]; + float currentPeak[MAX_AUDIO_CHANNELS]; + float currentInputPeak[MAX_AUDIO_CHANNELS]; + + int displayNrAudioChannels = 0; + float displayMagnitude[MAX_AUDIO_CHANNELS]; + float displayPeak[MAX_AUDIO_CHANNELS]; + float displayPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + float displayInputPeakHold[MAX_AUDIO_CHANNELS]; + uint64_t displayInputPeakHoldLastUpdateTime[MAX_AUDIO_CHANNELS]; + + QFont tickFont; + QColor backgroundNominalColor; + QColor backgroundWarningColor; + QColor backgroundErrorColor; + QColor foregroundNominalColor; + QColor foregroundWarningColor; + QColor foregroundErrorColor; + + QColor backgroundNominalColorDisabled; + QColor backgroundWarningColorDisabled; + QColor backgroundErrorColorDisabled; + QColor foregroundNominalColorDisabled; + QColor foregroundWarningColorDisabled; + QColor foregroundErrorColorDisabled; + + QColor clipColor; + QColor magnitudeColor; + QColor majorTickColor; + QColor minorTickColor; + + int meterThickness; + qreal meterFontScaling; + + qreal minimumLevel; + qreal warningLevel; + qreal errorLevel; + qreal clipLevel; + qreal minimumInputLevel; + qreal peakDecayRate; + qreal magnitudeIntegrationTime; + qreal peakHoldDuration; + qreal inputPeakHoldDuration; + + QColor p_backgroundNominalColor; + QColor p_backgroundWarningColor; + QColor p_backgroundErrorColor; + QColor p_foregroundNominalColor; + QColor p_foregroundWarningColor; + QColor p_foregroundErrorColor; + + uint64_t lastRedrawTime = 0; + int channels = 0; + bool clipping = false; + bool vertical; + bool muted = false; + +public: + explicit VolumeMeter(QWidget *parent = nullptr, obs_volmeter_t *obs_volmeter = nullptr, bool vertical = false); + ~VolumeMeter(); + + void setLevels(const float magnitude[MAX_AUDIO_CHANNELS], const float peak[MAX_AUDIO_CHANNELS], + const float inputPeak[MAX_AUDIO_CHANNELS]); + QRect getBarRect() const; + bool needLayoutChange(); + + QColor getBackgroundNominalColor() const; + void setBackgroundNominalColor(QColor c); + QColor getBackgroundWarningColor() const; + void setBackgroundWarningColor(QColor c); + QColor getBackgroundErrorColor() const; + void setBackgroundErrorColor(QColor c); + QColor getForegroundNominalColor() const; + void setForegroundNominalColor(QColor c); + QColor getForegroundWarningColor() const; + void setForegroundWarningColor(QColor c); + QColor getForegroundErrorColor() const; + void setForegroundErrorColor(QColor c); + + QColor getBackgroundNominalColorDisabled() const; + void setBackgroundNominalColorDisabled(QColor c); + QColor getBackgroundWarningColorDisabled() const; + void setBackgroundWarningColorDisabled(QColor c); + QColor getBackgroundErrorColorDisabled() const; + void setBackgroundErrorColorDisabled(QColor c); + QColor getForegroundNominalColorDisabled() const; + void setForegroundNominalColorDisabled(QColor c); + QColor getForegroundWarningColorDisabled() const; + void setForegroundWarningColorDisabled(QColor c); + QColor getForegroundErrorColorDisabled() const; + void setForegroundErrorColorDisabled(QColor c); + + QColor getClipColor() const; + void setClipColor(QColor c); + QColor getMagnitudeColor() const; + void setMagnitudeColor(QColor c); + QColor getMajorTickColor() const; + void setMajorTickColor(QColor c); + QColor getMinorTickColor() const; + void setMinorTickColor(QColor c); + int getMeterThickness() const; + void setMeterThickness(int v); + qreal getMeterFontScaling() const; + void setMeterFontScaling(qreal v); + qreal getMinimumLevel() const; + void setMinimumLevel(qreal v); + qreal getWarningLevel() const; + void setWarningLevel(qreal v); + qreal getErrorLevel() const; + void setErrorLevel(qreal v); + qreal getClipLevel() const; + void setClipLevel(qreal v); + qreal getMinimumInputLevel() const; + void setMinimumInputLevel(qreal v); + qreal getPeakDecayRate() const; + void setPeakDecayRate(qreal v); + qreal getMagnitudeIntegrationTime() const; + void setMagnitudeIntegrationTime(qreal v); + qreal getPeakHoldDuration() const; + void setPeakHoldDuration(qreal v); + qreal getInputPeakHoldDuration() const; + void setInputPeakHoldDuration(qreal v); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void wheelEvent(QWheelEvent *event) override; + +protected: + void paintEvent(QPaintEvent *event) override; + void changeEvent(QEvent *e) override; +}; + +class VolumeMeterTimer : public QTimer { + Q_OBJECT + +public: + inline VolumeMeterTimer() : QTimer() {} + + void AddVolControl(VolumeMeter *meter); + void RemoveVolControl(VolumeMeter *meter); + +protected: + void timerEvent(QTimerEvent *event) override; + QList volumeMeters; +}; + +class QLabel; +class VolumeSlider; +class MuteCheckBox; +class OBSSourceLabel; + +class VolControl : public QFrame { + Q_OBJECT + +private: + OBSSource source; + std::vector sigs; + OBSSourceLabel *nameLabel; + QLabel *volLabel; + VolumeMeter *volMeter; + VolumeSlider *slider; + MuteCheckBox *mute; + QPushButton *config = nullptr; + float levelTotal; + float levelCount; + OBSFader obs_fader; + OBSVolMeter obs_volmeter; + bool vertical; + QMenu *contextMenu; + + static void OBSVolumeChanged(void *param, float db); + static void OBSVolumeLevel(void *data, const float magnitude[MAX_AUDIO_CHANNELS], + const float peak[MAX_AUDIO_CHANNELS], const float inputPeak[MAX_AUDIO_CHANNELS]); + static void OBSVolumeMuted(void *data, calldata_t *calldata); + static void OBSMixersOrMonitoringChanged(void *data, calldata_t *); + + void EmitConfigClicked(); + +private slots: + void VolumeChanged(); + void VolumeMuted(bool muted); + void MixersOrMonitoringChanged(); + + void SetMuted(bool checked); + void SliderChanged(int vol); + void updateText(); + +signals: + void ConfigClicked(); + +public: + explicit VolControl(OBSSource source, bool showConfig = false, bool vertical = false); + ~VolControl(); + + inline obs_source_t *GetSource() const { return source; } + + void SetMeterDecayRate(qreal q); + void setPeakMeterType(enum obs_peak_meter_type peakMeterType); + + void EnableSlider(bool enable); + inline void SetContextMenu(QMenu *cm) { contextMenu = cm; } + + void refreshColors(); +}; + +class VolumeSlider : public AbsoluteSlider { + Q_OBJECT + +public: + obs_fader_t *fad; + + VolumeSlider(obs_fader_t *fader, QWidget *parent = nullptr); + VolumeSlider(obs_fader_t *fader, Qt::Orientation orientation, QWidget *parent = nullptr); + + bool getDisplayTicks() const; + void setDisplayTicks(bool display); + +private: + bool displayTicks = false; + QColor tickColor; + +protected: + virtual void paintEvent(QPaintEvent *event) override; +}; + +class VolumeAccessibleInterface : public QAccessibleWidget { + +public: + VolumeAccessibleInterface(QWidget *w); + + QVariant currentValue() const; + void setCurrentValue(const QVariant &value); + + QVariant maximumValue() const; + QVariant minimumValue() const; + + QVariant minimumStepSize() const; + +private: + VolumeSlider *slider() const; + +protected: + virtual QAccessible::Role role() const override; + virtual QString text(QAccessible::Text t) const override; +};